├── .github
├── dependabot.yml
└── workflows
│ ├── dependabot-auto-merge.yml
│ ├── main.yml
│ └── not-main.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .vscode
└── settings.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── bun.lock
├── package.json
├── src
├── all-extracted-declaration.test.ts
├── all-extracted-declaration.ts
├── ambient-modules-declarations.test.ts
├── ambient-modules-declarations.ts
├── apparent-type.test.ts
├── apparent-type.ts
├── bun-package-manager.test.ts
├── bun-package-manager.ts
├── create-project.test.ts
├── create-project.ts
├── docs.test.ts
├── docs.ts
├── export-equals-declarations.test.ts
├── export-equals-declarations.ts
├── exported-declarations.test.ts
├── exported-declarations.ts
├── extract-class.ts
├── extract-declarations.test.ts
├── extract-declarations.ts
├── extract-enum.ts
├── extract-expression.ts
├── extract-file-module.ts
├── extract-function-expression.ts
├── extract-function.ts
├── extract-interface.ts
├── extract-namespace.ts
├── extract-package-api-effect.ts
├── extract-package-api.test.ts
├── extract-package-api.ts
├── extract-type-alias.ts
├── extract-variable-assignment-expression.ts
├── extract-variable.ts
├── format-signature.test.ts
├── format-signature.ts
├── global-ambient-declarations.test.ts
├── global-ambient-declarations.ts
├── head-text.test.ts
├── head-text.ts
├── id.test.ts
├── id.ts
├── index.ts
├── is-class.test.ts
├── is-class.ts
├── is-enum.test.ts
├── is-enum.ts
├── is-exported-declarations.test.ts
├── is-exported-declarations.ts
├── is-expression.test.ts
├── is-expression.ts
├── is-file-module.test.ts
├── is-file-module.ts
├── is-function-expression.test.ts
├── is-function-expression.ts
├── is-function.test.ts
├── is-function.ts
├── is-global.test.ts
├── is-global.ts
├── is-hidden.test.ts
├── is-hidden.ts
├── is-interface.test.ts
├── is-interface.ts
├── is-namespace.test.ts
├── is-namespace.ts
├── is-shorthand-ambient-module.test.ts
├── is-shorthand-ambient-module.ts
├── is-type-alias.test.ts
├── is-type-alias.ts
├── is-variable-assignment-expression.test.ts
├── is-variable-assignment-expression.ts
├── is-variable.test.ts
├── is-variable.ts
├── modifiers-text.test.ts
├── modifiers-text.ts
├── package-declarations.test.ts
├── package-declarations.ts
├── package-json.test.ts
├── package-json.ts
├── package-manager.ts
├── package-name.test.ts
├── package-name.ts
├── package-overview.test.ts
├── package-overview.ts
├── package-types.test.ts
├── package-types.ts
├── parse-doc-comment.test.ts
├── parse-doc-comment.ts
├── source-file-path.test.ts
├── source-file-path.ts
├── type-checker-type.test.ts
├── type-checker-type.ts
├── work-dir.test.ts
└── work-dir.ts
├── tarballs
└── short-time-ago-3.0.0.tgz
├── test
├── declarations
│ ├── __snapshots__
│ │ ├── ambient-modules.test.ts.snap
│ │ ├── ambient-namespaces.test.ts.snap
│ │ ├── class-with-ambient-constructor.test.ts.snap
│ │ ├── class-with-overloaded-methods.test.ts.snap
│ │ ├── empty-index-file.test.ts.snap
│ │ ├── export-all-as-namespace.test.ts.snap
│ │ ├── export-as-namespace.test.ts.snap
│ │ ├── export-default-arrow-function.test.ts.snap
│ │ ├── export-default-class.test.ts.snap
│ │ ├── export-default-const.test.ts.snap
│ │ ├── export-default-expression.test.ts.snap
│ │ ├── export-default-function.test.ts.snap
│ │ ├── export-default-let-typed.test.ts.snap
│ │ ├── export-default-let.test.ts.snap
│ │ ├── export-default-named-class.test.ts.snap
│ │ ├── export-default-named-function.test.ts.snap
│ │ ├── export-default-nested-namespaces.test.ts.snap
│ │ ├── export-default-object.test.ts.snap
│ │ ├── export-equals-function-and-namespace.test.ts.snap
│ │ ├── export-equals-internal-declaration.test.ts.snap
│ │ ├── export-forms.test.ts.snap
│ │ ├── export-named-class-with-private-field.test.ts.snap
│ │ ├── export-named-declaration-without-jsdoc.test.ts.snap
│ │ ├── export-named-declarations.test.ts.snap
│ │ ├── export-named-import-all.test.ts.snap
│ │ ├── export-named-type-declarations.test.ts.snap
│ │ ├── export-type-import-type.test.ts.snap
│ │ ├── export-variable-and-type-with-identical-name.test.ts.snap
│ │ ├── index-file-with-overview.test.ts.snap
│ │ ├── interface-with-overloaded-methods.test.ts.snap
│ │ └── overloaded-function-with-multiple-docs.test.ts.snap
│ ├── ambient-modules.test.ts
│ ├── ambient-namespaces.test.ts
│ ├── class-with-ambient-constructor.test.ts
│ ├── class-with-overloaded-methods.test.ts
│ ├── empty-index-file.test.ts
│ ├── export-all-as-namespace.test.ts
│ ├── export-as-namespace.test.ts
│ ├── export-default-arrow-function.test.ts
│ ├── export-default-class.test.ts
│ ├── export-default-const.test.ts
│ ├── export-default-expression.test.ts
│ ├── export-default-function.test.ts
│ ├── export-default-let-typed.test.ts
│ ├── export-default-let.test.ts
│ ├── export-default-named-class.test.ts
│ ├── export-default-named-function.test.ts
│ ├── export-default-nested-namespaces.test.ts
│ ├── export-default-object.test.ts
│ ├── export-equals-function-and-namespace.test.ts
│ ├── export-equals-internal-declaration.test.ts
│ ├── export-forms.test.ts
│ ├── export-named-class-with-private-field.test.ts
│ ├── export-named-declaration-without-jsdoc.test.ts
│ ├── export-named-declarations.test.ts
│ ├── export-named-import-all.test.ts
│ ├── export-named-type-declarations.test.ts
│ ├── export-type-import-type.test.ts
│ ├── export-variable-and-type-with-identical-name.test.ts
│ ├── index-file-with-overview.test.ts
│ ├── interface-with-overloaded-methods.test.ts
│ └── overloaded-function-with-multiple-docs.test.ts
└── packages
│ ├── __snapshots__
│ ├── bundle-require.test.ts.snap
│ ├── enttec-open-dmx-usb.test.ts.snap
│ ├── exome.test.ts.snap
│ ├── faastjs.test.ts.snap
│ ├── h3.test.ts.snap
│ ├── highlight-words.test.ts.snap
│ ├── kirklin__eslint-config.test.ts.snap
│ ├── luxass__utils.test.ts.snap
│ ├── magicast.test.ts.snap
│ ├── microsoft__api-extractor.test.ts.snap
│ ├── npm.test.ts.snap
│ ├── preact.test.ts.snap
│ ├── query-registry.test.ts.snap
│ ├── sentry__browser.test.ts.snap
│ ├── short-time-ago.test.ts.snap
│ ├── storylite__storylite.test.ts.snap
│ ├── supeffective__dataset.test.ts.snap
│ ├── tinyargs.test.ts.snap
│ ├── ts-api-utils.test.ts.snap
│ ├── twoslash.test.ts.snap
│ ├── verify-hcaptcha.test.ts.snap
│ ├── violentmonkey__dom.test.ts.snap
│ ├── violentmonkey__shortcut.test.ts.snap
│ └── vue-email__nuxt.test.ts.snap
│ ├── bundle-require.test.ts
│ ├── enttec-open-dmx-usb.test.ts
│ ├── exome.test.ts
│ ├── faastjs.test.ts
│ ├── h3.test.ts
│ ├── highlight-words.test.ts
│ ├── kirklin__eslint-config.test.ts
│ ├── luxass__utils.test.ts
│ ├── magicast.test.ts
│ ├── microsoft__api-extractor.test.ts
│ ├── npm.test.ts
│ ├── preact.test.ts
│ ├── query-registry.test.ts
│ ├── sentry__browser.test.ts
│ ├── short-time-ago.test.ts
│ ├── storylite__storylite.test.ts
│ ├── supeffective__dataset.test.ts
│ ├── tinyargs.test.ts
│ ├── ts-api-utils.test.ts
│ ├── twoslash.test.ts
│ ├── verify-hcaptcha.test.ts
│ ├── violentmonkey__dom.test.ts
│ ├── violentmonkey__shortcut.test.ts
│ └── vue-email__nuxt.test.ts
├── tsconfig.json
├── tsup.config.ts
└── vitest.config.ts
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | enable-beta-ecosystems: true
8 | updates:
9 | - package-ecosystem: "github-actions"
10 | directory: "/"
11 | schedule:
12 | interval: "monthly"
13 |
14 | - package-ecosystem: "npm"
15 | directory: "/"
16 | schedule:
17 | interval: "monthly"
18 |
--------------------------------------------------------------------------------
/.github/workflows/dependabot-auto-merge.yml:
--------------------------------------------------------------------------------
1 | name: Dependabot auto-merge
2 | on: pull_request
3 |
4 | permissions:
5 | contents: write
6 | pull-requests: write
7 |
8 | jobs:
9 | dependabot:
10 | runs-on: ubuntu-latest
11 | if: ${{ github.actor == 'dependabot[bot]' }}
12 | steps:
13 | - name: Dependabot metadata
14 | id: metadata
15 | uses: dependabot/fetch-metadata@v2
16 | with:
17 | github-token: "${{ secrets.GITHUB_TOKEN }}"
18 | - name: Enable auto-merge for Dependabot PRs
19 | if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor'}}
20 | run: gh pr merge --auto --squash "$PR_URL"
21 | env:
22 | PR_URL: ${{github.event.pull_request.html_url}}
23 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
24 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI for main branch
2 |
3 | on:
4 | push:
5 | branches:
6 | - "main"
7 |
8 | jobs:
9 | # See https://stackoverflow.com/a/73624365/16109047
10 | build:
11 | runs-on: ubuntu-latest
12 | if: github.repository == 'jsdocs-io/extractor' && github.ref == 'refs/heads/main'
13 | steps:
14 | - name: Checkout repository
15 | uses: actions/checkout@v4
16 |
17 | - name: Setup bun
18 | uses: oven-sh/setup-bun@v2
19 |
20 | - name: Setup Node
21 | uses: actions/setup-node@v4
22 | with:
23 | node-version: lts/*
24 |
25 | - name: Install dependencies
26 | run: bun install --frozen-lockfile
27 |
28 | - name: Lint
29 | run: bun run lint
30 |
31 | - name: Test
32 | run: bun run test:ci
33 |
34 | - name: Build
35 | run: bun run build
36 |
37 | - name: Are the types wrong
38 | run: bun run attw
39 |
40 | - name: CodeCov
41 | uses: codecov/codecov-action@v5.4.3
42 | with:
43 | token: ${{ secrets.CODECOV_TOKEN }}
44 |
--------------------------------------------------------------------------------
/.github/workflows/not-main.yml:
--------------------------------------------------------------------------------
1 | name: CI for non-main branches
2 |
3 | on:
4 | push:
5 | branches-ignore:
6 | - "main"
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout repository
13 | uses: actions/checkout@v4
14 |
15 | - name: Setup bun
16 | uses: oven-sh/setup-bun@v2
17 |
18 | - name: Setup Node
19 | uses: actions/setup-node@v4
20 | with:
21 | node-version: lts/*
22 |
23 | - name: Install dependencies
24 | run: bun install --frozen-lockfile
25 |
26 | - name: Lint
27 | run: bun run lint
28 |
29 | - name: Test
30 | run: bun run test:ci
31 |
32 | - name: Build
33 | run: bun run build
34 |
35 | - name: Are the types wrong
36 | run: bun run attw
37 |
38 | - name: CodeCov
39 | uses: codecov/codecov-action@v5.4.3
40 | with:
41 | token: ${{ secrets.CODECOV_TOKEN }}
42 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .DS_Store
3 | node_modules
4 | dist
5 | coverage
6 | clinic
7 | .clinic
8 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | coverage
4 | dist
5 | bun.lock
6 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": true,
3 | "printWidth": 100,
4 | "experimentalTernaries": true
5 | }
6 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.ignoreWords": [
3 | "arethetypeswrong",
4 | "attw",
5 | "codecov",
6 | "commitlint",
7 | "edoardo",
8 | "execa",
9 | "exome",
10 | "foobarbaz",
11 | "jsdocs",
12 | "keyof",
13 | "kirklin",
14 | "luxass",
15 | "modifierable",
16 | "neverthrow",
17 | "overloadable",
18 | "pathe",
19 | "pollyjs",
20 | "preact",
21 | "scibona",
22 | "storylite",
23 | "supeffective",
24 | "tempy",
25 | "tsdoc",
26 | "tsup",
27 | "velut",
28 | "violentmonkey",
29 | "vitest"
30 | ],
31 | "typescript.tsdk": "node_modules/typescript/lib"
32 | }
33 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [Unreleased]
9 |
10 | ### Changed
11 |
12 | - String union type `DeclarationKind` replaces enum `DeclarationKinds`.
13 | - Support only Node >= 20.
14 | - ESM only
15 |
16 | ## [0.4.0] - 2022-08-15
17 |
18 | ### Changed
19 |
20 | - Updated dependencies
21 |
22 | ## [0.3.0] - 2022-01-17
23 |
24 | ### Changed
25 |
26 | - Updated dependencies
27 | - Better type inference and signatures formatting thanks to TypeScript and Prettier updates
28 |
29 | ## [0.2.0] - 2021-04-23
30 |
31 | ### Added
32 |
33 | - Skip API extraction from packages with `skipAPIExtraction` option in `analyzeRegistryPackage`
34 |
35 | ## [0.1.0] - 2021-04-20
36 |
37 | ### Added
38 |
39 | - Initial release
40 |
41 | [unreleased]: https://github.com/jsdocs-io/extractor/compare/v0.4.0...HEAD
42 | [0.4.0]: https://github.com/jsdocs-io/extractor/compare/v0.3.0...v0.4.0
43 | [0.3.0]: https://github.com/jsdocs-io/extractor/compare/v0.2.0...v0.3.0
44 | [0.2.0]: https://github.com/jsdocs-io/extractor/compare/v0.1.0...v0.2.0
45 | [0.1.0]: https://github.com/jsdocs-io/extractor/releases/tag/v0.1.0
46 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # @jsdocs-io/extractor
2 |
3 | [](https://github.com/jsdocs-io/extractor/actions/workflows/main.yml?query=workflow%3ACI)
4 | [](https://codecov.io/gh/jsdocs-io/extractor)
5 | [](https://www.jsdocs.io/package/@jsdocs-io/extractor)
6 | 
7 | [](https://www.npmjs.com/package/@jsdocs-io/extractor)
8 | [](https://github.com/jsdocs-io/extractor/blob/main/LICENSE)
9 |
10 | This is the API extractor powering [**jsDocs.io**](https://www.jsdocs.io).
11 |
12 | It downloads packages from the npm registry and analyzes them to extract their public API.
13 |
14 | ## Requirements
15 |
16 | - [Bun](https://bun.sh/) must be installed to resolve and install packages
17 |
18 | ## API & Package Info
19 |
20 | - Explore the API on [**jsDocs.io**](https://www.jsdocs.io/package/@jsdocs-io/extractor)
21 | - View package contents on [**unpkg**](https://unpkg.com/@jsdocs-io/extractor/)
22 | - View repository on [**GitHub**](https://github.com/jsdocs-io/extractor)
23 | - Read the changelog on [**GitHub**](https://github.com/jsdocs-io/extractor/blob/main/CHANGELOG.md)
24 |
25 | ## Usage Examples
26 |
27 | > [!WARNING]
28 | > Analyzing packages is a blocking operation that requires some time, even seconds, to finish!
29 | > Using workers is recommended.
30 |
31 | 1. Analyze the latest version of the `preact` package from the npm registry:
32 |
33 | ```ts
34 | import { extractPackageApi } from "@jsdocs-io/extractor";
35 |
36 | (async () => {
37 | const packageApi = await extractPackageApi({ pkg: "preact" });
38 | console.log(JSON.stringify(packageApi, null, 2));
39 | })();
40 | ```
41 |
42 | 2. Analyze a specific [subpath export](https://nodejs.org/api/packages.html#subpath-exports), like `preact/hooks`:
43 |
44 | ```ts
45 | import { extractPackageApi } from "@jsdocs-io/extractor";
46 |
47 | (async () => {
48 | const result = await extractPackageApi({ pkg: "preact", subpath: "hooks" });
49 | console.log(JSON.stringify(packageApi, null, 2));
50 | })();
51 | ```
52 |
53 | ## License
54 |
55 | AGPL-3.0-or-later
56 |
57 | Copyright (C) 2025 Edoardo Scibona. See [LICENSE](LICENSE).
58 |
59 | This program is free software: you can redistribute it and/or modify
60 | it under the terms of the GNU Affero General Public License as published by
61 | the Free Software Foundation, either version 3 of the License, or
62 | (at your option) any later version.
63 |
64 | This program is distributed in the hope that it will be useful,
65 | but WITHOUT ANY WARRANTY; without even the implied warranty of
66 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
67 | GNU Affero General Public License for more details.
68 |
69 | You should have received a copy of the GNU Affero General Public License
70 | along with this program. If not, see .
71 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@jsdocs-io/extractor",
3 | "version": "1.0.0",
4 | "description": "The API extractor for npm packages powering jsdocs.io",
5 | "license": "AGPL-3.0-or-later",
6 | "author": {
7 | "name": "Edoardo Scibona",
8 | "url": "http://github.com/velut"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/jsdocs-io/extractor.git"
13 | },
14 | "bugs": {
15 | "url": "https://github.com/jsdocs-io/extractor/issues"
16 | },
17 | "keywords": [
18 | "npm",
19 | "registry",
20 | "package",
21 | "analyze",
22 | "api",
23 | "extractor",
24 | "documentation",
25 | "generator",
26 | "typescript",
27 | "javascript",
28 | "jsdoc",
29 | "tsdoc",
30 | "jsdocs.io"
31 | ],
32 | "sideEffects": false,
33 | "type": "module",
34 | "types": "./dist/index.d.ts",
35 | "exports": {
36 | ".": {
37 | "types": "./dist/index.d.ts",
38 | "import": "./dist/index.js"
39 | },
40 | "./package.json": "./package.json"
41 | },
42 | "files": [
43 | "dist"
44 | ],
45 | "engines": {
46 | "node": ">=20"
47 | },
48 | "scripts": {
49 | "check": "tsc --noEmit",
50 | "build": "tsc --noEmit && tsup",
51 | "attw": "attw --pack . --ignore-rules cjs-resolves-to-esm",
52 | "test": "bun --revision && vitest run src test/declarations --coverage --bail 1",
53 | "test:ci": "bun --revision && vitest run src test/declarations test/packages --coverage --bail 1",
54 | "test:update": "bun --revision && vitest run src test/declarations test/packages --update",
55 | "lint": "prettier --check .",
56 | "format": "prettier --write .",
57 | "pre-push": "bun --revision && bun i && bun run lint && bun run build && bun run test:ci && bun run attw",
58 | "release": "np --no-cleanup"
59 | },
60 | "dependencies": {
61 | "@microsoft/tsdoc": "^0.15.1",
62 | "effect": "^3.15.2",
63 | "execa": "^9.5.3",
64 | "memoize": "^10.1.0",
65 | "natural-orderby": "^5.0.0",
66 | "pathe": "^2.0.3",
67 | "prettier": "^3.5.3",
68 | "read-pkg": "^9.0.1",
69 | "resolve.exports": "^2.0.3",
70 | "tempy": "^3.1.0",
71 | "ts-morph": "^26.0.0",
72 | "validate-npm-package-name": "^6.0.0"
73 | },
74 | "devDependencies": {
75 | "@arethetypeswrong/cli": "^0.18.1",
76 | "@total-typescript/shoehorn": "^0.1.2",
77 | "@types/node": "^22.15.21",
78 | "@types/validate-npm-package-name": "^4.0.2",
79 | "@vitest/coverage-v8": "^3.1.4",
80 | "np": "^10.2.0",
81 | "ts-dedent": "^2.2.0",
82 | "tsup": "^8.5.0",
83 | "typescript": "^5.8.3",
84 | "vitest": "^3.1.4"
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/all-extracted-declaration.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest";
2 | import * as allExtractedDeclaration from "./all-extracted-declaration";
3 |
4 | test("ok", () => {
5 | const declaration: allExtractedDeclaration.AllExtractedDeclaration = {
6 | kind: "variable",
7 | id: "",
8 | name: "",
9 | docs: [],
10 | file: "",
11 | line: 0,
12 | signature: "",
13 | };
14 | expect(declaration.kind).toBe("variable");
15 | });
16 |
--------------------------------------------------------------------------------
/src/all-extracted-declaration.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | ExtractedClass,
3 | ExtractedClassConstructor,
4 | ExtractedClassMethod,
5 | ExtractedClassProperty,
6 | } from "./extract-class";
7 | import type { ExtractedEnum, ExtractedEnumMember } from "./extract-enum";
8 | import type { ExtractedFunction } from "./extract-function";
9 | import type {
10 | ExtractedInterface,
11 | ExtractedInterfaceCallSignature,
12 | ExtractedInterfaceConstructSignature,
13 | ExtractedInterfaceGetAccessor,
14 | ExtractedInterfaceIndexSignature,
15 | ExtractedInterfaceMethod,
16 | ExtractedInterfaceProperty,
17 | ExtractedInterfaceSetAccessor,
18 | } from "./extract-interface";
19 | import type { ExtractedNamespace } from "./extract-namespace";
20 | import type { ExtractedTypeAlias } from "./extract-type-alias";
21 | import type { ExtractedVariable } from "./extract-variable";
22 |
23 | /**
24 | `AllExtractedDeclaration` is the union of all possible declarations
25 | that can be extracted, with some being found only in other declarations
26 | (e.g., class method declarations are found only in a class declaration).
27 | */
28 | export type AllExtractedDeclaration =
29 | | ExtractedVariable
30 | | ExtractedFunction
31 | | ExtractedClass
32 | | ExtractedClassConstructor
33 | | ExtractedClassProperty
34 | | ExtractedClassMethod
35 | | ExtractedInterface
36 | | ExtractedInterfaceProperty
37 | | ExtractedInterfaceMethod
38 | | ExtractedInterfaceConstructSignature
39 | | ExtractedInterfaceCallSignature
40 | | ExtractedInterfaceIndexSignature
41 | | ExtractedInterfaceGetAccessor
42 | | ExtractedInterfaceSetAccessor
43 | | ExtractedEnum
44 | | ExtractedEnumMember
45 | | ExtractedTypeAlias
46 | | ExtractedNamespace;
47 |
48 | /**
49 | `AllExtractedDeclarationKind` is the union of all discriminators
50 | used to detect the kind of declaration.
51 | */
52 | export type AllExtractedDeclarationKind = AllExtractedDeclaration["kind"];
53 |
--------------------------------------------------------------------------------
/src/ambient-modules-declarations.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { ambientModulesDeclarations } from "./ambient-modules-declarations";
5 |
6 | test("no ambient modules", () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | export {};
20 | `,
21 | );
22 | expect(ambientModulesDeclarations("", project)).toStrictEqual([]);
23 | });
24 |
25 | test("hidden ambient module", () => {
26 | const project = new Project({
27 | useInMemoryFileSystem: true,
28 | compilerOptions: {
29 | lib: ["lib.esnext.full.d.ts"],
30 | target: ScriptTarget.ESNext,
31 | module: ModuleKind.ESNext,
32 | moduleResolution: ModuleResolutionKind.Bundler,
33 | },
34 | });
35 | project.createSourceFile(
36 | "index.d.ts",
37 | dedent`
38 | /** @internal */
39 | declare module "foo";
40 | `,
41 | );
42 | expect(ambientModulesDeclarations("", project)).toStrictEqual([]);
43 | });
44 |
45 | test("ambient module from another package", () => {
46 | const project = new Project({
47 | useInMemoryFileSystem: true,
48 | compilerOptions: {
49 | lib: ["lib.esnext.full.d.ts"],
50 | target: ScriptTarget.ESNext,
51 | module: ModuleKind.ESNext,
52 | moduleResolution: ModuleResolutionKind.Bundler,
53 | },
54 | });
55 | project.createSourceFile(
56 | "/node_modules/other-package/index.d.ts",
57 | dedent`
58 | declare module "foo";
59 | `,
60 | );
61 | expect(ambientModulesDeclarations("", project, "my-package")).toStrictEqual([]);
62 | });
63 |
64 | test("shorthand ambient module", () => {
65 | const project = new Project({
66 | useInMemoryFileSystem: true,
67 | compilerOptions: {
68 | lib: ["lib.esnext.full.d.ts"],
69 | target: ScriptTarget.ESNext,
70 | module: ModuleKind.ESNext,
71 | moduleResolution: ModuleResolutionKind.Bundler,
72 | },
73 | });
74 | project.createSourceFile(
75 | "/index.d.ts",
76 | dedent`
77 | /** Module "foo" */
78 | declare module "foo";
79 | `,
80 | );
81 | const ambientModules = ambientModulesDeclarations("", project);
82 | expect(ambientModules.length).toBe(1);
83 | expect(ambientModules[0]?.exportName).toBe('"foo"');
84 | });
85 |
86 | test("ambient modules", () => {
87 | const project = new Project({
88 | useInMemoryFileSystem: true,
89 | compilerOptions: {
90 | lib: ["lib.esnext.full.d.ts"],
91 | target: ScriptTarget.ESNext,
92 | module: ModuleKind.ESNext,
93 | moduleResolution: ModuleResolutionKind.Bundler,
94 | },
95 | });
96 | project.createSourceFile(
97 | "/index.d.ts",
98 | dedent`
99 | /** Module 'bar' */
100 | declare module "bar" {
101 | /** Variable var1 */
102 | const var1: string;
103 |
104 | /** Variable var2 */
105 | export const var2: boolean;
106 | }
107 |
108 | /** Module 'foo bar' */
109 | declare module "foo bar" {
110 | export default function sum(a: number, b: number): number;
111 | }
112 | `,
113 | );
114 | const ambientModules = ambientModulesDeclarations("", project);
115 | expect(ambientModules.length).toBe(2);
116 | expect(ambientModules[0]?.exportName).toBe('"bar"');
117 | expect(ambientModules[1]?.exportName).toBe('"foo bar"');
118 | });
119 |
--------------------------------------------------------------------------------
/src/ambient-modules-declarations.ts:
--------------------------------------------------------------------------------
1 | import { ModuleDeclaration, Node, type Project } from "ts-morph";
2 | import { isHidden } from "./is-hidden";
3 | import { sourceFilePath } from "./source-file-path";
4 |
5 | export type AmbientModulesDeclarationsReturn = {
6 | containerName: string;
7 | exportName: string;
8 | declaration: ModuleDeclaration;
9 | }[];
10 |
11 | export const ambientModulesDeclarations = (
12 | containerName: string,
13 | project: Project,
14 | pkgName?: string,
15 | ): AmbientModulesDeclarationsReturn => {
16 | const ambientModulesDeclarations = [];
17 | for (const symbol of project.getAmbientModules()) {
18 | for (const declaration of symbol.getDeclarations()) {
19 | if (isHidden(declaration) || !Node.isModuleDeclaration(declaration)) {
20 | continue;
21 | }
22 | if (pkgName && !sourceFilePath(declaration).startsWith(`/${pkgName}`)) {
23 | // Ignore ambient modules that are not from the analyzed package.
24 | continue;
25 | }
26 | const exportName = declaration.getName();
27 | ambientModulesDeclarations.push({
28 | containerName,
29 | exportName,
30 | declaration,
31 | });
32 | }
33 | }
34 | return ambientModulesDeclarations;
35 | };
36 |
--------------------------------------------------------------------------------
/src/apparent-type.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { apparentType } from "./apparent-type";
5 |
6 | test("apparent type", () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | const foo = 42;
20 | `,
21 | );
22 | expect(apparentType(indexFile.getVariableDeclarationOrThrow("foo"))).toBe("number");
23 | });
24 |
--------------------------------------------------------------------------------
/src/apparent-type.ts:
--------------------------------------------------------------------------------
1 | import { TypeFormatFlags, ts, type Node } from "ts-morph";
2 |
3 | export const apparentType = (node: Node): string =>
4 | // See:
5 | // https://github.com/dsherret/ts-morph/issues/453#issuecomment-427405736
6 | // https://github.com/dsherret/ts-morph/issues/453#issuecomment-667578386
7 | node
8 | .getType()
9 | .getApparentType()
10 | .getText(
11 | node,
12 | ts.TypeFormatFlags.NoTruncation | TypeFormatFlags.UseAliasDefinedOutsideCurrentScope,
13 | )
14 | .replace(/^Number$/, "number")
15 | .replace(/^Boolean$/, "boolean")
16 | .replace(/^String$/, "string");
17 |
--------------------------------------------------------------------------------
/src/bun-package-manager.test.ts:
--------------------------------------------------------------------------------
1 | import { Effect } from "effect";
2 | import { temporaryDirectoryTask } from "tempy";
3 | import { expect, test } from "vitest";
4 | import { bunPackageManager } from "./bun-package-manager";
5 | import type { InstallPackageOptions } from "./package-manager";
6 |
7 | const bun = bunPackageManager();
8 |
9 | const _installPackage = (options: InstallPackageOptions) =>
10 | Effect.runPromise(bun.installPackage(options));
11 |
12 | test("invalid package", async () => {
13 | await temporaryDirectoryTask(async (cwd) => {
14 | await expect(_installPackage({ pkg: "", cwd })).rejects.toThrow();
15 | });
16 | });
17 |
18 | test("package with no production dependencies", async () => {
19 | await temporaryDirectoryTask(async (cwd) => {
20 | await expect(_installPackage({ pkg: "verify-hcaptcha@1.0.0", cwd })).resolves.toStrictEqual([
21 | "verify-hcaptcha@1.0.0",
22 | ]);
23 | });
24 | });
25 |
26 | test("package with some production dependencies", async () => {
27 | await temporaryDirectoryTask(async (cwd) => {
28 | await expect(_installPackage({ pkg: "query-registry@2.6.0", cwd })).resolves.toContain(
29 | "query-registry@2.6.0",
30 | );
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/src/bun-package-manager.ts:
--------------------------------------------------------------------------------
1 | import { Effect } from "effect";
2 | import { execa } from "execa";
3 | import { InstallPackageError, PackageManager } from "./package-manager";
4 |
5 | /** @internal */
6 | export const bunPackageManager = (bunPath = "bun") =>
7 | PackageManager.of({
8 | installPackage: ({ pkg, cwd }) =>
9 | Effect.gen(function* () {
10 | // Run `bun add --verbose`.
11 | // See https://bun.sh/docs/cli/add.
12 | const { stdout } = yield* Effect.tryPromise({
13 | try: () => execa(bunPath, ["add", pkg, "--verbose"], { cwd }),
14 | catch: (e) => new InstallPackageError({ cause: e }),
15 | });
16 |
17 | // With verbose output on, bun prints one line per installed package
18 | // (e.g., "foo@1.0.0"), including all installed dependencies.
19 | // These lines are between the two delimiting lines found here:
20 | // https://github.com/oven-sh/bun/blob/972a7b7080bd3066b54dcb43e9c91c5dfa26a69c/src/install/lockfile.zig#L5369-L5370.
21 | const lines = stdout.split("\n");
22 | const beginHash = lines.findIndex((line) => line.startsWith("-- BEGIN SHA512/256"));
23 | const endHash = lines.findIndex((line) => line.startsWith("-- END HASH"));
24 | const installedPackages = lines.slice(beginHash + 1, endHash);
25 | return installedPackages;
26 | }),
27 | });
28 |
--------------------------------------------------------------------------------
/src/create-project.test.ts:
--------------------------------------------------------------------------------
1 | import { Effect } from "effect";
2 | import fs from "node:fs/promises";
3 | import { join } from "pathe";
4 | import { temporaryDirectoryTask } from "tempy";
5 | import { expect, test } from "vitest";
6 | import { createProject, type CreateProjectOptions } from "./create-project";
7 |
8 | const _createProject = (options: CreateProjectOptions) => Effect.runPromise(createProject(options));
9 |
10 | test("no cwd", async () => {
11 | await temporaryDirectoryTask(async (dir) => {
12 | await expect(
13 | _createProject({
14 | indexFilePath: "./this-file-does-not-exist.ts",
15 | cwd: join(dir, "this-dir-does-not-exist"),
16 | }),
17 | ).rejects.toThrow();
18 | });
19 | });
20 |
21 | test("no index file", async () => {
22 | await temporaryDirectoryTask(async (dir) => {
23 | await expect(
24 | _createProject({
25 | indexFilePath: "./this-file-does-not-exist.ts",
26 | cwd: dir,
27 | }),
28 | ).rejects.toThrow();
29 | });
30 | });
31 |
32 | test("with index file", async () => {
33 | await temporaryDirectoryTask(async (dir) => {
34 | const indexFilePath = join(dir, "./index.ts");
35 | await fs.writeFile(indexFilePath, "export {};");
36 | const { project } = await _createProject({ indexFilePath, cwd: dir });
37 | expect(project.getSourceFiles().map((sf) => sf.getBaseName())).toStrictEqual(["index.ts"]);
38 | });
39 | });
40 |
41 | test("with index file and other file", async () => {
42 | await temporaryDirectoryTask(async (dir) => {
43 | const indexFilePath = join(dir, "./index.ts");
44 | const otherFilePath = join(dir, "./other.ts");
45 | await fs.writeFile(indexFilePath, "export * from './other';");
46 | await fs.writeFile(otherFilePath, "export const a = 1;");
47 | const { project } = await _createProject({ indexFilePath, cwd: dir });
48 | expect(project.getSourceFiles().map((sf) => sf.getBaseName())).toStrictEqual([
49 | "index.ts",
50 | "other.ts",
51 | ]);
52 | });
53 | });
54 |
--------------------------------------------------------------------------------
/src/create-project.ts:
--------------------------------------------------------------------------------
1 | import { Data, Effect } from "effect";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 |
4 | export type CreateProjectOptions = {
5 | indexFilePath: string;
6 | cwd: string;
7 | };
8 |
9 | /** @internal */
10 | export class ProjectError extends Data.TaggedError("ProjectError")<{
11 | readonly cause?: unknown;
12 | }> {}
13 |
14 | export const createProject = ({ indexFilePath, cwd }: CreateProjectOptions) =>
15 | Effect.try({
16 | try: () => {
17 | const project = new Project({
18 | compilerOptions: {
19 | // See https://github.com/dsherret/ts-morph/issues/938
20 | // and https://github.com/microsoft/TypeScript/blob/master/lib/lib.esnext.full.d.ts
21 | lib: ["lib.esnext.full.d.ts"],
22 | target: ScriptTarget.ESNext,
23 | module: ModuleKind.ESNext,
24 | moduleResolution: ModuleResolutionKind.Bundler,
25 | // By default, ts-morph creates a project rooted in the current working directory.
26 | // We must change the `typeRoots` directory to the temporary directory
27 | // where the packages are installed, otherwise TypeScript will discover
28 | // `@types` packages from our local `node_modules` directory.
29 | // See https://www.typescriptlang.org/tsconfig#typeRoots.
30 | typeRoots: [cwd],
31 | },
32 | });
33 | const indexFile = project.addSourceFileAtPath(indexFilePath);
34 | project.resolveSourceFileDependencies();
35 | return { project, indexFile };
36 | },
37 | catch: (e) => new ProjectError({ cause: e }),
38 | });
39 |
--------------------------------------------------------------------------------
/src/docs.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { docs } from "./docs";
5 |
6 | test("docs", () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | /** This @packageDocumentation is not the docs for foo */
20 | export const foo: string;
21 |
22 | /** Docs for bar */
23 | export const bar: string;
24 |
25 | /** Docs for expression */
26 | export default 42;
27 |
28 | /** Docs for function overloads 1 */
29 | declare function fooFunc(a: number): number;
30 |
31 | /** Docs for function overloads 2 */
32 | declare function fooFunc(a: string): string;
33 |
34 | class FooClass {
35 | /** Docs for class method overloads 1 */
36 | fooMethod(a: number): void;
37 |
38 | /** Docs for class method overloads 2 */
39 | fooMethod(a: string | number) {}
40 | }
41 |
42 | interface FooInterface {
43 | /** Docs for interface method overloads 1 */
44 | fooMethod(a: number): number;
45 |
46 | /** Docs for interface method overloads 2 */
47 | fooMethod(a: string): string;
48 | }
49 |
50 | /** Docs for Qux */
51 | export type Qux = {};
52 | `,
53 | );
54 | expect(docs(indexFile.getVariableDeclarationOrThrow("foo"))).toStrictEqual([]);
55 | expect(docs(indexFile.getVariableDeclarationOrThrow("bar"))).toStrictEqual([
56 | "/** Docs for bar */",
57 | ]);
58 | expect(docs(indexFile.getExportedDeclarations().get("default")?.at(0)!)).toStrictEqual([
59 | "/** Docs for expression */",
60 | ]);
61 | expect(docs(indexFile.getFunctionOrThrow("fooFunc"))).toStrictEqual([
62 | "/** Docs for function overloads 1 */",
63 | "/** Docs for function overloads 2 */",
64 | ]);
65 | expect(docs(indexFile.getClassOrThrow("FooClass").getMethodOrThrow("fooMethod"))).toStrictEqual([
66 | "/** Docs for class method overloads 1 */",
67 | "/** Docs for class method overloads 2 */",
68 | ]);
69 | expect(
70 | docs(indexFile.getInterfaceOrThrow("FooInterface").getMethodOrThrow("fooMethod")),
71 | ).toStrictEqual([
72 | "/** Docs for interface method overloads 1 */",
73 | "/** Docs for interface method overloads 2 */",
74 | ]);
75 | expect(docs(indexFile.getTypeAliasOrThrow("Qux"))).toStrictEqual(["/** Docs for Qux */"]);
76 | });
77 |
--------------------------------------------------------------------------------
/src/docs.ts:
--------------------------------------------------------------------------------
1 | import { Node, SyntaxKind } from "ts-morph";
2 | import { parseDocComment } from "./parse-doc-comment";
3 |
4 | export const docs = (node: Node): string[] => [
5 | // List of unique jsdoc comments that are closest to the node.
6 | ...new Set(nodesWithDocs(node).flatMap((node) => lastDoc(node) ?? [])),
7 | ];
8 |
9 | const nodesWithDocs = (node: Node): Node[] => {
10 | // A node may not have the jsdoc comment directly attached to it in the AST.
11 | // Let's find the nodes that do so starting from the given node.
12 | if (Node.isVariableDeclaration(node)) {
13 | return [node.getVariableStatementOrThrow()];
14 | }
15 | if (Node.isExpression(node)) {
16 | return [node.getParent()!];
17 | }
18 | if (Node.isOverloadable(node) && !Node.isConstructorDeclaration(node)) {
19 | // Functions and class methods can be overloaded with each declaration
20 | // having its own doc. Since we return overloaded declarations grouped
21 | // into one item, we need this item to share the docs of all overloads.
22 | // Constructors can also be overloaded but they are excluded because
23 | // one item is returned per constructor each with its own docs.
24 | const implementation = node.getImplementation();
25 | return [...node.getOverloads(), ...(implementation ? [implementation] : [])];
26 | }
27 | if (
28 | Node.isMethodSignature(node) &&
29 | node.getParent().getKind() === SyntaxKind.InterfaceDeclaration
30 | ) {
31 | // Treat interface methods like overloadable class methods above.
32 | const methodName = node.getName();
33 | const overloads = node
34 | .getParentIfKindOrThrow(SyntaxKind.InterfaceDeclaration)
35 | .getMethods()
36 | .filter((method) => method.getName() === methodName);
37 | return overloads;
38 | }
39 | return [node];
40 | };
41 |
42 | const lastDoc = (node: Node): string | undefined => {
43 | // Get the jsdoc comment closest to the node.
44 | const doc = node.getLastChildByKind(SyntaxKind.JSDoc)?.getText();
45 | if (!doc) {
46 | return undefined;
47 | }
48 | if (parseDocComment(doc).modifierTagSet.isPackageDocumentation()) {
49 | // The first declaration after package documentation should not
50 | // inherit that unrelated jsdoc comment if it has none itself.
51 | // See `export-named-declaration-without-jsdoc.test.ts`.
52 | return undefined;
53 | }
54 | return doc;
55 | };
56 |
--------------------------------------------------------------------------------
/src/export-equals-declarations.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { exportEqualsDeclarations } from "./export-equals-declarations";
5 |
6 | test("shorthand ambient module", () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.d.ts",
18 | dedent`
19 | declare module 'foo';
20 | `,
21 | );
22 | expect(exportEqualsDeclarations("", indexFile.getModuleOrThrow("'foo'"))).toStrictEqual([]);
23 | });
24 |
25 | test("export default", () => {
26 | const project = new Project({
27 | useInMemoryFileSystem: true,
28 | compilerOptions: {
29 | lib: ["lib.esnext.full.d.ts"],
30 | target: ScriptTarget.ESNext,
31 | module: ModuleKind.ESNext,
32 | moduleResolution: ModuleResolutionKind.Bundler,
33 | },
34 | });
35 | const indexFile = project.createSourceFile(
36 | "index.ts",
37 | dedent`
38 | export default function() {}
39 | `,
40 | );
41 | expect(exportEqualsDeclarations("", indexFile)).toStrictEqual([]);
42 | });
43 |
44 | test("export equals expression", () => {
45 | const project = new Project({
46 | useInMemoryFileSystem: true,
47 | compilerOptions: {
48 | lib: ["lib.esnext.full.d.ts"],
49 | target: ScriptTarget.ESNext,
50 | module: ModuleKind.ESNext,
51 | moduleResolution: ModuleResolutionKind.Bundler,
52 | },
53 | });
54 | const indexFile = project.createSourceFile(
55 | "index.ts",
56 | dedent`
57 | export = 42;
58 | `,
59 | );
60 | expect(exportEqualsDeclarations("", indexFile)).toStrictEqual([]);
61 | });
62 |
63 | test("export equals identifier", () => {
64 | const project = new Project({
65 | useInMemoryFileSystem: true,
66 | compilerOptions: {
67 | lib: ["lib.esnext.full.d.ts"],
68 | target: ScriptTarget.ESNext,
69 | module: ModuleKind.ESNext,
70 | moduleResolution: ModuleResolutionKind.Bundler,
71 | },
72 | });
73 | const indexFile = project.createSourceFile(
74 | "index.ts",
75 | dedent`
76 | declare function foo(s: string): func1.Interface1;
77 | export = foo;
78 | `,
79 | );
80 | expect(exportEqualsDeclarations("", indexFile).at(0)?.exportName).toBe("foo");
81 | });
82 |
83 | test("export equals identifier hidden", () => {
84 | const project = new Project({
85 | useInMemoryFileSystem: true,
86 | compilerOptions: {
87 | lib: ["lib.esnext.full.d.ts"],
88 | target: ScriptTarget.ESNext,
89 | module: ModuleKind.ESNext,
90 | moduleResolution: ModuleResolutionKind.Bundler,
91 | },
92 | });
93 | const indexFile = project.createSourceFile(
94 | "index.ts",
95 | dedent`
96 | /** @internal */
97 | declare function foo(s: string): func1.Interface1;
98 | export = foo;
99 | `,
100 | );
101 | expect(exportEqualsDeclarations("", indexFile)).toStrictEqual([]);
102 | });
103 |
104 | test("export equals identifier namespace", () => {
105 | const project = new Project({
106 | useInMemoryFileSystem: true,
107 | compilerOptions: {
108 | lib: ["lib.esnext.full.d.ts"],
109 | target: ScriptTarget.ESNext,
110 | module: ModuleKind.ESNext,
111 | moduleResolution: ModuleResolutionKind.Bundler,
112 | },
113 | });
114 | const indexFile = project.createSourceFile(
115 | "index.ts",
116 | dedent`
117 | declare namespace foo {};
118 | export = foo;
119 | `,
120 | );
121 | expect(exportEqualsDeclarations("", indexFile)).toStrictEqual([]);
122 | });
123 |
--------------------------------------------------------------------------------
/src/export-equals-declarations.ts:
--------------------------------------------------------------------------------
1 | import { ModuleDeclaration, SourceFile, SyntaxKind, type ExportedDeclarations } from "ts-morph";
2 | import { isExportedDeclarations } from "./is-exported-declarations";
3 | import { isHidden } from "./is-hidden";
4 | import { isNamespace } from "./is-namespace";
5 | import { isShorthandAmbientModule } from "./is-shorthand-ambient-module";
6 |
7 | export type ExportEqualsDeclarationsReturn = {
8 | containerName: string;
9 | exportName: string;
10 | declaration: Exclude;
11 | }[];
12 |
13 | export const exportEqualsDeclarations = (
14 | containerName: string,
15 | container: SourceFile | ModuleDeclaration,
16 | ): ExportEqualsDeclarationsReturn => {
17 | if (isShorthandAmbientModule(container)) {
18 | // Export equals declarations may exist inside the body of ambient modules.
19 | // However, this is impossible for shorthand ambient modules with no body.
20 | return [];
21 | }
22 | const exportIdentifier = container
23 | .getExportAssignment((assignment) => assignment.isExportEquals())
24 | ?.getLastChildByKind(SyntaxKind.Identifier);
25 | if (!exportIdentifier) {
26 | return [];
27 | }
28 | const exportName = exportIdentifier.getText();
29 | const exportEqualsDeclarations = [];
30 | for (const declaration of exportIdentifier.getDefinitionNodes()) {
31 | if (isHidden(declaration) || !isExportedDeclarations(declaration)) {
32 | continue;
33 | }
34 | if (isNamespace(declaration)) {
35 | // Skip namespaces since `exportedDeclarations()` already extracts
36 | // the inner declarations of an export equals namespace as
37 | // non-namespaced declarations belonging to the parent container.
38 | // See snapshot for `export-equals-function-and-namespace.test.ts`.
39 | continue;
40 | }
41 | exportEqualsDeclarations.push({ containerName, exportName, declaration });
42 | }
43 | return exportEqualsDeclarations;
44 | };
45 |
--------------------------------------------------------------------------------
/src/exported-declarations.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { exportedDeclarations } from "./exported-declarations";
5 |
6 | test("no exports", () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | export {};
20 | `,
21 | );
22 | expect(exportedDeclarations("", indexFile)).toStrictEqual([]);
23 | });
24 |
25 | test("hidden export", () => {
26 | const project = new Project({
27 | useInMemoryFileSystem: true,
28 | compilerOptions: {
29 | lib: ["lib.esnext.full.d.ts"],
30 | target: ScriptTarget.ESNext,
31 | module: ModuleKind.ESNext,
32 | moduleResolution: ModuleResolutionKind.Bundler,
33 | },
34 | });
35 | const indexFile = project.createSourceFile(
36 | "index.ts",
37 | dedent`
38 | /** @internal */
39 | export const foo: string;
40 | `,
41 | );
42 | expect(exportedDeclarations("", indexFile)).toStrictEqual([]);
43 | });
44 |
45 | test("visible export", () => {
46 | const project = new Project({
47 | useInMemoryFileSystem: true,
48 | compilerOptions: {
49 | lib: ["lib.esnext.full.d.ts"],
50 | target: ScriptTarget.ESNext,
51 | module: ModuleKind.ESNext,
52 | moduleResolution: ModuleResolutionKind.Bundler,
53 | },
54 | });
55 | const indexFile = project.createSourceFile(
56 | "index.ts",
57 | dedent`
58 | export const foo: string;
59 | `,
60 | );
61 | expect(exportedDeclarations("", indexFile).at(0)?.exportName).toBe("foo");
62 | });
63 |
64 | test("multiple exports", () => {
65 | const project = new Project({
66 | useInMemoryFileSystem: true,
67 | compilerOptions: {
68 | lib: ["lib.esnext.full.d.ts"],
69 | target: ScriptTarget.ESNext,
70 | module: ModuleKind.ESNext,
71 | moduleResolution: ModuleResolutionKind.Bundler,
72 | },
73 | });
74 | project.createSourceFile(
75 | "file-module.ts",
76 | dedent`
77 | export {};
78 | `,
79 | );
80 | const indexFile = project.createSourceFile(
81 | "index.ts",
82 | dedent`
83 | export const fooVar: string;
84 | export function fooFunc() {}
85 | export class FooClass {}
86 | export interface FooInterface {}
87 | export enum FooEnum {}
88 | export type FooType = {};
89 | export namespace FooNamespace {}
90 | export * as fooModule from './file-module';
91 | export default 42;
92 | `,
93 | );
94 | expect(exportedDeclarations("", indexFile).length).toBe(9);
95 | });
96 |
--------------------------------------------------------------------------------
/src/exported-declarations.ts:
--------------------------------------------------------------------------------
1 | import type { ExportedDeclarations, ModuleDeclaration, SourceFile } from "ts-morph";
2 | import { isExportedDeclarations } from "./is-exported-declarations";
3 | import { isHidden } from "./is-hidden";
4 |
5 | export type ExportedDeclarationsReturn = {
6 | containerName: string;
7 | exportName: string;
8 | declaration: ExportedDeclarations;
9 | }[];
10 |
11 | export const exportedDeclarations = (
12 | containerName: string,
13 | container: SourceFile | ModuleDeclaration,
14 | ): ExportedDeclarationsReturn => {
15 | const exportedDeclarations = [];
16 | for (const [exportName, declarations] of container.getExportedDeclarations()) {
17 | for (const declaration of declarations) {
18 | if (isHidden(declaration) || !isExportedDeclarations(declaration)) {
19 | continue;
20 | }
21 | exportedDeclarations.push({ containerName, exportName, declaration });
22 | }
23 | }
24 | return exportedDeclarations;
25 | };
26 |
--------------------------------------------------------------------------------
/src/extract-enum.ts:
--------------------------------------------------------------------------------
1 | import { EnumMember, type EnumDeclaration } from "ts-morph";
2 | import { docs } from "./docs";
3 | import { formatSignature } from "./format-signature";
4 | import { headText } from "./head-text";
5 | import { id } from "./id";
6 | import { isHidden } from "./is-hidden";
7 | import { sourceFilePath } from "./source-file-path";
8 |
9 | export type ExtractedEnum = {
10 | kind: "enum";
11 | id: string;
12 | name: string;
13 | docs: string[];
14 | file: string;
15 | line: number;
16 | signature: string;
17 | members: ExtractedEnumMember[];
18 | };
19 |
20 | export type ExtractedEnumMember = {
21 | kind: "enum-member";
22 | id: string;
23 | name: string;
24 | docs: string[];
25 | file: string;
26 | line: number;
27 | signature: string;
28 | };
29 |
30 | export const extractEnum = async (
31 | containerName: string,
32 | exportName: string,
33 | declaration: EnumDeclaration,
34 | ): Promise => {
35 | const enumId = id(containerName, "+enum", exportName);
36 | return {
37 | kind: "enum",
38 | id: enumId,
39 | name: exportName,
40 | docs: docs(declaration),
41 | file: sourceFilePath(declaration),
42 | line: declaration.getStartLineNumber(),
43 | signature: await enumSignature(declaration),
44 | members: await extractEnumMembers(enumId, declaration),
45 | };
46 | };
47 |
48 | const enumSignature = async (declaration: EnumDeclaration): Promise => {
49 | const signature = headText(declaration);
50 | return await formatSignature("enum", signature);
51 | };
52 |
53 | const extractEnumMembers = async (
54 | enumId: string,
55 | enumDeclaration: EnumDeclaration,
56 | ): Promise => {
57 | const members = [];
58 | for (const declaration of enumDeclaration.getMembers()) {
59 | if (isHidden(declaration)) {
60 | continue;
61 | }
62 | const name = declaration.getName();
63 | members.push({
64 | kind: "enum-member" as const,
65 | id: id(enumId, "+member", name),
66 | name,
67 | docs: docs(declaration),
68 | file: sourceFilePath(declaration),
69 | line: declaration.getStartLineNumber(),
70 | signature: await enumMemberSignature(declaration),
71 | });
72 | }
73 | return members;
74 | };
75 |
76 | const enumMemberSignature = async (declaration: EnumMember): Promise => {
77 | const signature = declaration.getText();
78 | return await formatSignature("enum-member", signature);
79 | };
80 |
--------------------------------------------------------------------------------
/src/extract-expression.ts:
--------------------------------------------------------------------------------
1 | import type { Expression } from "ts-morph";
2 | import { apparentType } from "./apparent-type";
3 | import { docs } from "./docs";
4 | import type { ExtractedVariable } from "./extract-variable";
5 | import { formatSignature } from "./format-signature";
6 | import { id } from "./id";
7 | import { sourceFilePath } from "./source-file-path";
8 |
9 | export const extractExpression = async (
10 | containerName: string,
11 | exportName: string,
12 | declaration: Expression,
13 | ): Promise => ({
14 | kind: "variable",
15 | id: id(containerName, "+variable", exportName),
16 | name: exportName,
17 | docs: docs(declaration),
18 | file: sourceFilePath(declaration),
19 | line: declaration.getStartLineNumber(),
20 | signature: await expressionSignature(exportName, declaration),
21 | });
22 |
23 | const expressionSignature = async (name: string, declaration: Expression): Promise => {
24 | const kind = "const";
25 | const type = apparentType(declaration);
26 | return await formatSignature("variable", `${kind} ${name}: ${type}`);
27 | };
28 |
--------------------------------------------------------------------------------
/src/extract-file-module.ts:
--------------------------------------------------------------------------------
1 | import { SourceFile, SyntaxKind } from "ts-morph";
2 | import type { ExtractedDeclaration } from "./extract-declarations";
3 | import type { ExtractedNamespace } from "./extract-namespace";
4 | import { formatSignature } from "./format-signature";
5 | import { id } from "./id";
6 | import { sourceFilePath } from "./source-file-path";
7 |
8 | export const extractFileModule = async (
9 | containerName: string,
10 | exportName: string,
11 | declaration: SourceFile,
12 | declarations: ExtractedDeclaration[],
13 | ): Promise => ({
14 | kind: "namespace",
15 | id: id(containerName, "+namespace", exportName),
16 | name: exportName,
17 | docs: fileModuleDocs(declaration),
18 | file: sourceFilePath(declaration),
19 | line: declaration.getStartLineNumber(),
20 | signature: await fileModuleSignature(declaration),
21 | declarations,
22 | });
23 |
24 | const fileModuleDocs = (declaration: SourceFile): string[] => {
25 | const firstDoc = declaration.getFirstDescendantByKind(SyntaxKind.JSDoc)?.getText();
26 | if (!firstDoc) {
27 | return [];
28 | }
29 | return [firstDoc];
30 | };
31 |
32 | const fileModuleSignature = async (declaration: SourceFile): Promise => {
33 | const filename = declaration.getSourceFile().getBaseName();
34 | return await formatSignature("namespace", `module "${filename}" {}`);
35 | };
36 |
--------------------------------------------------------------------------------
/src/extract-function-expression.ts:
--------------------------------------------------------------------------------
1 | import type { VariableDeclaration } from "ts-morph";
2 | import { docs } from "./docs";
3 | import type { ExtractedFunction } from "./extract-function";
4 | import { formatSignature } from "./format-signature";
5 | import { id } from "./id";
6 | import { sourceFilePath } from "./source-file-path";
7 | import { typeCheckerType } from "./type-checker-type";
8 |
9 | export const extractFunctionExpression = async (
10 | containerName: string,
11 | exportName: string,
12 | declaration: VariableDeclaration,
13 | ): Promise => ({
14 | kind: "function",
15 | id: id(containerName, "+function", exportName),
16 | name: exportName,
17 | docs: docs(declaration),
18 | file: sourceFilePath(declaration),
19 | line: declaration.getStartLineNumber(),
20 | signature: await functionExpressionSignature(exportName, declaration),
21 | });
22 |
23 | const functionExpressionSignature = async (
24 | name: string,
25 | declaration: VariableDeclaration,
26 | ): Promise => {
27 | const type = typeCheckerType(declaration);
28 | return await formatSignature("function", `${name}: ${type}`);
29 | };
30 |
--------------------------------------------------------------------------------
/src/extract-function.ts:
--------------------------------------------------------------------------------
1 | import type { ArrowFunction, FunctionDeclaration } from "ts-morph";
2 | import { docs } from "./docs";
3 | import { formatSignature } from "./format-signature";
4 | import { id } from "./id";
5 | import { sourceFilePath } from "./source-file-path";
6 | import { typeCheckerType } from "./type-checker-type";
7 |
8 | export type ExtractedFunction = {
9 | kind: "function";
10 | id: string;
11 | name: string;
12 | docs: string[];
13 | file: string;
14 | line: number;
15 | signature: string;
16 | };
17 |
18 | export const extractFunction = async (
19 | containerName: string,
20 | exportName: string,
21 | declaration: FunctionDeclaration | ArrowFunction,
22 | ): Promise => ({
23 | kind: "function",
24 | id: id(containerName, "+function", exportName),
25 | name: exportName,
26 | docs: docs(declaration),
27 | file: sourceFilePath(declaration),
28 | line: declaration.getStartLineNumber(),
29 | signature: await functionSignature(exportName, declaration),
30 | });
31 |
32 | const functionSignature = async (
33 | name: string,
34 | declaration: FunctionDeclaration | ArrowFunction,
35 | ): Promise => {
36 | const type = typeCheckerType(declaration);
37 | return await formatSignature("function", `${name}: ${type}`);
38 | };
39 |
--------------------------------------------------------------------------------
/src/extract-namespace.ts:
--------------------------------------------------------------------------------
1 | import { ModuleDeclaration } from "ts-morph";
2 | import { docs } from "./docs";
3 | import type { ExtractedDeclaration } from "./extract-declarations";
4 | import { formatSignature } from "./format-signature";
5 | import { id } from "./id";
6 | import { sourceFilePath } from "./source-file-path";
7 |
8 | export type ExtractedNamespace = {
9 | kind: "namespace";
10 | id: string;
11 | name: string;
12 | docs: string[];
13 | file: string;
14 | line: number;
15 | signature: string;
16 | declarations: ExtractedDeclaration[];
17 | };
18 |
19 | export const extractNamespace = async (
20 | containerName: string,
21 | exportName: string,
22 | declaration: ModuleDeclaration,
23 | declarations: ExtractedDeclaration[],
24 | ): Promise => ({
25 | kind: "namespace",
26 | id: id(containerName, "+namespace", exportName),
27 | name: exportName,
28 | docs: docs(declaration),
29 | file: sourceFilePath(declaration),
30 | line: declaration.getStartLineNumber(),
31 | signature: await namespaceSignature(exportName),
32 | declarations,
33 | });
34 |
35 | const namespaceSignature = async (exportName: string): Promise => {
36 | const containerKeyword =
37 | exportName.startsWith('"') || exportName.startsWith("'") ? "module" : "namespace";
38 | const signature = `${containerKeyword} ${exportName} {}`;
39 | return await formatSignature("namespace", signature);
40 | };
41 |
--------------------------------------------------------------------------------
/src/extract-package-api-effect.ts:
--------------------------------------------------------------------------------
1 | import { Effect } from "effect";
2 | import { performance } from "node:perf_hooks";
3 | import { join } from "pathe";
4 | import { createProject } from "./create-project";
5 | import type { ExtractPackageApiOptions, PackageApi } from "./extract-package-api";
6 | import { packageDeclarations } from "./package-declarations";
7 | import { packageJson } from "./package-json";
8 | import { PackageManager } from "./package-manager";
9 | import { packageOverview } from "./package-overview";
10 | import { packageTypes } from "./package-types";
11 | import { workDir } from "./work-dir";
12 |
13 | /** @internal */
14 | export const extractPackageApiEffect = ({
15 | pkg,
16 | subpath = ".",
17 | maxDepth = 5,
18 | }: Omit) =>
19 | Effect.gen(function* () {
20 | const startTime = performance.now();
21 | const { path: cwd } = yield* workDir;
22 | const pm = yield* PackageManager;
23 | const packages = yield* pm.installPackage({ pkg, cwd });
24 | const workDirPkgJson = yield* packageJson(cwd);
25 | const pkgName = Object.keys(workDirPkgJson.dependencies!)[0]!;
26 | const pkgDir = join(cwd, "node_modules", pkgName);
27 | const pkgJson = yield* packageJson(pkgDir);
28 | const types = yield* packageTypes(pkgJson, subpath);
29 | const indexFilePath = join(pkgDir, types);
30 | const { project, indexFile } = yield* createProject({ indexFilePath, cwd });
31 | const overview = packageOverview(indexFile);
32 | const declarations = yield* packageDeclarations({ pkgName, project, indexFile, maxDepth });
33 | const pkgApi: PackageApi = {
34 | name: pkgJson.name,
35 | version: pkgJson.version,
36 | subpath,
37 | types,
38 | overview,
39 | declarations,
40 | packages,
41 | analyzedAt: new Date().toISOString(),
42 | analyzedIn: Math.round(performance.now() - startTime),
43 | };
44 | return pkgApi;
45 | });
46 |
--------------------------------------------------------------------------------
/src/extract-package-api.test.ts:
--------------------------------------------------------------------------------
1 | import { join } from "pathe";
2 | import { expect, test } from "vitest";
3 | import { extractPackageApi } from "./extract-package-api";
4 |
5 | test("invalid package name", async () => {
6 | const startDir = process.cwd();
7 | await expect(extractPackageApi({ pkg: "" })).rejects.toThrow();
8 | expect(process.cwd()).toBe(startDir);
9 | });
10 |
11 | test("npm package not found", async () => {
12 | const startDir = process.cwd();
13 | await expect(extractPackageApi({ pkg: "@jsdocs-io/not-found" })).rejects.toThrow();
14 | expect(process.cwd()).toBe(startDir);
15 | });
16 |
17 | test("npm package types not found", async () => {
18 | const startDir = process.cwd();
19 | await expect(extractPackageApi({ pkg: "unlicensed@0.4.0" })).rejects.toThrow();
20 | expect(process.cwd()).toBe(startDir);
21 | });
22 |
23 | test("npm package successfully analyzed", async () => {
24 | const startDir = process.cwd();
25 | await expect(extractPackageApi({ pkg: "short-time-ago@2.0.0" })).resolves.toMatchObject({
26 | name: "short-time-ago",
27 | version: "2.0.0",
28 | });
29 | expect(process.cwd()).toBe(startDir);
30 | });
31 |
32 | test("local tarball package successfully analyzed", async () => {
33 | const startDir = process.cwd();
34 | await expect(
35 | extractPackageApi({ pkg: join(startDir, "tarballs/short-time-ago-3.0.0.tgz") }),
36 | ).resolves.toMatchObject({
37 | name: "short-time-ago",
38 | version: "3.0.0",
39 | });
40 | expect(process.cwd()).toBe(startDir);
41 | });
42 |
--------------------------------------------------------------------------------
/src/extract-package-api.ts:
--------------------------------------------------------------------------------
1 | import { Effect } from "effect";
2 | import { bunPackageManager } from "./bun-package-manager";
3 | import type { ExtractedDeclaration } from "./extract-declarations";
4 | import { extractPackageApiEffect } from "./extract-package-api-effect";
5 | import { PackageManager } from "./package-manager";
6 |
7 | /**
8 | `ExtractPackageApiOptions` contains all the options
9 | for calling {@link extractPackageApi}.
10 | */
11 | export type ExtractPackageApiOptions = {
12 | /**
13 | Package to extract the API from.
14 |
15 | This can be either a package name (e.g., `foo`) or any other query
16 | that can be passed to `bun add` (e.g., `foo@1.0.0`).
17 |
18 | @see {@link https://bun.sh/docs/cli/add | Bun docs}
19 | */
20 | pkg: string;
21 |
22 | /**
23 | Specific subpath to consider in a package.
24 |
25 | If a package has multiple entrypoints listed in the `exports` property
26 | of its `package.json`, use `subpath` to select a specific one by its name
27 | (e.g., `someFeature`).
28 |
29 | @defaultValue `.` (package root)
30 |
31 | @see {@link https://nodejs.org/api/packages.html#subpath-exports | Node.js docs}
32 | @see {@link https://github.com/lukeed/resolve.exports | resolve.exports docs}
33 | */
34 | subpath?: string;
35 |
36 | /**
37 | Packages can have deeply nested modules and namespaces.
38 |
39 | Use `maxDepth` to limit the depth of the extraction.
40 | Declarations nested at levels deeper than this value will be ignored.
41 |
42 | @defaultValue 5
43 | */
44 | maxDepth?: number;
45 |
46 | /**
47 | Absolute path to the `bun` executable.
48 | Used to locate bun if it's not in `PATH`.
49 |
50 | @defaultValue `bun`
51 | */
52 | bunPath?: string;
53 | };
54 |
55 | /**
56 | `PackageApi` contains all the information extracted from a package.
57 | */
58 | export type PackageApi = {
59 | /** Package name (e.g., `foo`). */
60 | name: string;
61 |
62 | /** Package version number (e.g., `1.0.0`). */
63 | version: string;
64 |
65 | /**
66 | Package subpath selected when extracting the API (e.g., `.`, `someFeature`).
67 |
68 | @see {@link ExtractPackageApiOptions.subpath}
69 | @see {@link https://nodejs.org/api/packages.html#subpath-exports | Node.js docs}
70 | */
71 | subpath: string;
72 |
73 | /**
74 | Type declarations file, resolved from the selected `subpath`,
75 | that acts as the entrypoint for the package (e.g., `index.d.ts`).
76 | */
77 | types: string;
78 |
79 | /**
80 | Package description extracted from the `types` file if a
81 | JSDoc comment with the `@packageDocumentation` tag is found.
82 | */
83 | overview: string | undefined;
84 |
85 | /** Declarations exported (or re-exported) by the package. */
86 | declarations: ExtractedDeclaration[];
87 |
88 | /**
89 | All packages resolved and installed when installing the package (included).
90 |
91 | @example
92 | ```ts
93 | ["foo@1.0.0", "bar@2.0.0", "baz@3.0.0"]
94 | ```
95 | */
96 | packages: string[];
97 |
98 | /** Timestamp of when the package was analyzed. */
99 | analyzedAt: string;
100 |
101 | /** Package analysis duration in milliseconds. */
102 | analyzedIn: number;
103 | };
104 |
105 | /**
106 | `extractPackageApi` extracts the API from a package.
107 |
108 | If the extraction succeeds, `extractPackageApi` returns a {@link PackageApi} object.
109 | If the extraction fails, `extractPackageApi` throws an error.
110 |
111 | Warning: The extraction process is slow and blocks the main thread, using workers is recommended.
112 |
113 | @example
114 | ```ts
115 | const packageApi = await extractPackageApi({
116 | pkg: "foo", // Extract API from npm package `foo` [Required]
117 | subpath: ".", // Select subpath `.` (root subpath) [Optional]
118 | maxDepth: 5, // Maximum depth for analyzing nested namespaces [Optional]
119 | bunPath: "bun" // Absolute path to the `bun` executable [Optional]
120 | });
121 | console.log(JSON.stringify(packageApi, null, 2));
122 | ```
123 |
124 | @param options - {@link ExtractPackageApiOptions}
125 |
126 | @returns A {@link PackageApi} object
127 | */
128 | export const extractPackageApi = async ({
129 | pkg,
130 | subpath = ".",
131 | maxDepth = 5,
132 | bunPath = "bun",
133 | }: ExtractPackageApiOptions): Promise => {
134 | return await extractPackageApiEffect({ pkg, subpath, maxDepth }).pipe(
135 | Effect.scoped,
136 | Effect.provideService(PackageManager, bunPackageManager(bunPath)),
137 | Effect.runPromise,
138 | );
139 | };
140 |
--------------------------------------------------------------------------------
/src/extract-type-alias.ts:
--------------------------------------------------------------------------------
1 | import type { TypeAliasDeclaration } from "ts-morph";
2 | import { docs } from "./docs";
3 | import { formatSignature } from "./format-signature";
4 | import { id } from "./id";
5 | import { sourceFilePath } from "./source-file-path";
6 |
7 | export type ExtractedTypeAlias = {
8 | kind: "type";
9 | id: string;
10 | name: string;
11 | docs: string[];
12 | file: string;
13 | line: number;
14 | signature: string;
15 | };
16 |
17 | export const extractTypeAlias = async (
18 | containerName: string,
19 | exportName: string,
20 | declaration: TypeAliasDeclaration,
21 | ): Promise => ({
22 | kind: "type",
23 | id: id(containerName, "+type", exportName),
24 | name: exportName,
25 | docs: docs(declaration),
26 | file: sourceFilePath(declaration),
27 | line: declaration.getStartLineNumber(),
28 | signature: await typeAliasSignature(declaration),
29 | });
30 |
31 | const typeAliasSignature = async (declaration: TypeAliasDeclaration): Promise => {
32 | // Using `declaration.getType().getText(declaration);` returns the expanded/resolved type.
33 | // However this causes:
34 | // - inline imports like `import('...').SomeType` to appear in the signature
35 | // - types can become really long in some cases as they are expanded by the compiler
36 | // - format flags like `TypeFormatFlags.NoTruncation | TypeFormatFlags.UseAliasDefinedOutsideCurrentScope`
37 | // seem to be ignored or cause local types to be resolved to their name like `type Foo = Foo;`
38 | // For these reasons we simply get the type alias text as written by authors.
39 | // See:
40 | // - https://github.com/dsherret/ts-morph/issues/453#issuecomment-427405736
41 | // - https://twitter.com/drosenwasser/status/1289640180035403776
42 | const signature = declaration.getText();
43 | return await formatSignature("type", signature);
44 | };
45 |
--------------------------------------------------------------------------------
/src/extract-variable-assignment-expression.ts:
--------------------------------------------------------------------------------
1 | import type { BinaryExpression, VariableDeclaration } from "ts-morph";
2 | import { apparentType } from "./apparent-type";
3 | import { docs } from "./docs";
4 | import type { ExtractedVariable } from "./extract-variable";
5 | import { formatSignature } from "./format-signature";
6 | import { id } from "./id";
7 | import { sourceFilePath } from "./source-file-path";
8 |
9 | export const extractVariableAssignmentExpression = async (
10 | containerName: string,
11 | exportName: string,
12 | declaration: BinaryExpression,
13 | ): Promise => {
14 | const variableDeclaration = declaration
15 | .getLeft()
16 | .getSymbol()!
17 | .getDeclarations()[0] as VariableDeclaration;
18 | return {
19 | kind: "variable",
20 | id: id(containerName, "+variable", exportName),
21 | name: exportName,
22 | docs: docs(variableDeclaration),
23 | file: sourceFilePath(variableDeclaration),
24 | line: variableDeclaration.getStartLineNumber(),
25 | signature: await variableAssignmentExpressionSignature(
26 | exportName,
27 | declaration,
28 | variableDeclaration,
29 | ),
30 | };
31 | };
32 |
33 | const variableAssignmentExpressionSignature = async (
34 | name: string,
35 | declaration: BinaryExpression,
36 | variableDeclaration: VariableDeclaration,
37 | ): Promise => {
38 | const kind = variableDeclaration.getVariableStatementOrThrow().getDeclarationKind().toString();
39 | const variableType = apparentType(variableDeclaration);
40 | const expressionType = apparentType(declaration);
41 | const type = variableType !== "any" ? variableType : expressionType;
42 | return await formatSignature("variable", `${kind} ${name}: ${type}`);
43 | };
44 |
--------------------------------------------------------------------------------
/src/extract-variable.ts:
--------------------------------------------------------------------------------
1 | import type { VariableDeclaration } from "ts-morph";
2 | import { apparentType } from "./apparent-type";
3 | import { docs } from "./docs";
4 | import { formatSignature } from "./format-signature";
5 | import { id } from "./id";
6 | import { sourceFilePath } from "./source-file-path";
7 |
8 | export type ExtractedVariable = {
9 | kind: "variable";
10 | id: string;
11 | name: string;
12 | docs: string[];
13 | file: string;
14 | line: number;
15 | signature: string;
16 | };
17 |
18 | export const extractVariable = async (
19 | containerName: string,
20 | exportName: string,
21 | declaration: VariableDeclaration,
22 | ): Promise => ({
23 | kind: "variable",
24 | id: id(containerName, "+variable", exportName),
25 | name: exportName,
26 | docs: docs(declaration),
27 | file: sourceFilePath(declaration),
28 | line: declaration.getStartLineNumber(),
29 | signature: await variableSignature(exportName, declaration),
30 | });
31 |
32 | const variableSignature = async (
33 | name: string,
34 | declaration: VariableDeclaration,
35 | ): Promise => {
36 | const kind = declaration.getVariableStatementOrThrow().getDeclarationKind().toString();
37 | const type = apparentType(declaration);
38 | return await formatSignature("variable", `${kind} ${name}: ${type}`);
39 | };
40 |
--------------------------------------------------------------------------------
/src/format-signature.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest";
2 | import { formatSignature } from "./format-signature";
3 |
4 | test("format signature", async () => {
5 | const tests = [
6 | ["variable", "const foo: string", "const foo: string;"],
7 | ["function", "foo: () => string", "foo: () => string;"],
8 | ["class", "class Foo {}", "class Foo {}"],
9 | ["interface", "interface Foo {}", "interface Foo {}"],
10 | ["enum", "enum Foo {}", "enum Foo {}"],
11 | ["class-constructor", "constructor() {}", "constructor() {}"],
12 | ["interface-property", "foo: string", "foo: string;"],
13 | ] as const;
14 | for (const tc of tests) {
15 | expect(await formatSignature(tc[0], tc[1])).toBe(tc[2]);
16 | }
17 | });
18 |
19 | test("formatting error", async () => {
20 | expect(await formatSignature("variable", "{")).toBe("{");
21 | });
22 |
--------------------------------------------------------------------------------
/src/format-signature.ts:
--------------------------------------------------------------------------------
1 | import { format } from "prettier";
2 | import type { AllExtractedDeclarationKind } from "./all-extracted-declaration";
3 |
4 | export const formatSignature = async (
5 | kind: AllExtractedDeclarationKind,
6 | signature: string,
7 | ): Promise => {
8 | const s = signature
9 | .trim()
10 | // Remove unnecessary keywords at declaration start.
11 | .replace(/^export\s+/, "")
12 | .replace(/^default\s+/, "")
13 | .replace(/^declare\s+/, "");
14 | switch (kind) {
15 | case "variable": {
16 | // Temporarily replace the invalid variable name `default` before the type.
17 | const raw = s.replace("default:", "_______:");
18 | const formatted = await formatWithPrettier(raw);
19 | return formatted.replace("_______:", "default:");
20 | }
21 | case "function": {
22 | // Temporarily add `let` to format the function like a variable signature.
23 | const raw = `let ${s.replace("default:", "_______:")}`;
24 | const formatted = await formatWithPrettier(raw);
25 | return formatted.replace("_______:", "default:").replace(/^let\s/, "");
26 | }
27 | case "class":
28 | case "interface":
29 | case "enum":
30 | case "type":
31 | case "namespace": {
32 | // Just format the signature as-is.
33 | const formatted = await formatWithPrettier(s);
34 | return formatted;
35 | }
36 | case "class-constructor":
37 | case "class-property":
38 | case "class-method":
39 | case "interface-property":
40 | case "interface-method":
41 | case "interface-construct-signature":
42 | case "interface-call-signature":
43 | case "interface-index-signature":
44 | case "interface-get-accessor":
45 | case "interface-set-accessor":
46 | case "enum-member": {
47 | // Temporarily wrap members in their parent declaration for formatting.
48 | const parentKind = kind.split("-")[0]!;
49 | const raw = `${parentKind} P { ${s} }`;
50 | const formattedParent = await formatWithPrettier(raw);
51 | const formatted = formattedParent
52 | // Remove parent wrapper on first and last lines.
53 | .split("\n")
54 | .slice(1, -1)
55 | // Remove indentation.
56 | .map((line) => line.replace(/^\s{2}/, ""))
57 | .join("\n")
58 | // Remove trailing comma in enum member signature.
59 | .replace(/,$/, "");
60 | return formatted;
61 | }
62 | }
63 | };
64 |
65 | const formatWithPrettier = async (s: string): Promise => {
66 | try {
67 | const formatted = (await format(s, { parser: "typescript" })).trim();
68 | return formatted;
69 | } catch {
70 | return s;
71 | }
72 | };
73 |
--------------------------------------------------------------------------------
/src/global-ambient-declarations.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { globalAmbientDeclarations } from "./global-ambient-declarations";
5 |
6 | test("no globals", () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.d.ts",
18 | dedent`
19 | export {};
20 | `,
21 | );
22 | expect(globalAmbientDeclarations("", indexFile)).toStrictEqual([]);
23 | });
24 |
25 | test("not global", () => {
26 | const project = new Project({
27 | useInMemoryFileSystem: true,
28 | compilerOptions: {
29 | lib: ["lib.esnext.full.d.ts"],
30 | target: ScriptTarget.ESNext,
31 | module: ModuleKind.ESNext,
32 | moduleResolution: ModuleResolutionKind.Bundler,
33 | },
34 | });
35 | const indexFile = project.createSourceFile(
36 | "index.d.ts",
37 | dedent`
38 | export const foo: number;
39 | `,
40 | );
41 | expect(globalAmbientDeclarations("", indexFile)).toStrictEqual([]);
42 | });
43 |
44 | test("hidden global", () => {
45 | const project = new Project({
46 | useInMemoryFileSystem: true,
47 | compilerOptions: {
48 | lib: ["lib.esnext.full.d.ts"],
49 | target: ScriptTarget.ESNext,
50 | module: ModuleKind.ESNext,
51 | moduleResolution: ModuleResolutionKind.Bundler,
52 | },
53 | });
54 | const indexFile = project.createSourceFile(
55 | "index.d.ts",
56 | dedent`
57 | /** @internal */
58 | declare const foo: number;
59 | `,
60 | );
61 | expect(globalAmbientDeclarations("", indexFile)).toStrictEqual([]);
62 | });
63 |
64 | test("global declaration", () => {
65 | const project = new Project({
66 | useInMemoryFileSystem: true,
67 | compilerOptions: {
68 | lib: ["lib.esnext.full.d.ts"],
69 | target: ScriptTarget.ESNext,
70 | module: ModuleKind.ESNext,
71 | moduleResolution: ModuleResolutionKind.Bundler,
72 | },
73 | });
74 | const indexFile = project.createSourceFile(
75 | "index.d.ts",
76 | dedent`
77 | declare const foo: number;
78 | `,
79 | );
80 | expect(globalAmbientDeclarations("", indexFile).at(0)?.exportName).toBe("foo");
81 | });
82 |
--------------------------------------------------------------------------------
/src/global-ambient-declarations.ts:
--------------------------------------------------------------------------------
1 | import {
2 | SourceFile,
3 | type FunctionDeclaration,
4 | type ModuleDeclaration,
5 | type VariableDeclaration,
6 | } from "ts-morph";
7 | import { isGlobal } from "./is-global";
8 | import { isHidden } from "./is-hidden";
9 |
10 | export type GlobalAmbientDeclarationsReturn = {
11 | containerName: string;
12 | exportName: string;
13 | declaration: VariableDeclaration | FunctionDeclaration | ModuleDeclaration;
14 | }[];
15 |
16 | export const globalAmbientDeclarations = (
17 | containerName: string,
18 | container: SourceFile,
19 | ): GlobalAmbientDeclarationsReturn => {
20 | // See https://www.typescriptlang.org/docs/handbook/declaration-files/by-example.html#global-variables.
21 | const globalCandidates = [
22 | ...container.getVariableDeclarations(),
23 | ...container.getFunctions(),
24 | ...container.getModules(),
25 | ];
26 | const globalAmbientDeclarations = [];
27 | for (const declaration of globalCandidates) {
28 | if (isHidden(declaration) || !isGlobal(declaration)) {
29 | continue;
30 | }
31 | globalAmbientDeclarations.push({
32 | containerName,
33 | // Global ambient functions must have a name.
34 | exportName: declaration.getName()!,
35 | declaration,
36 | });
37 | }
38 | return globalAmbientDeclarations;
39 | };
40 |
--------------------------------------------------------------------------------
/src/head-text.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { headText } from "./head-text";
5 |
6 | test("return text from declaration before body without JSDoc", () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | /** Docs for FooInterface */
20 | interface FooInterface {
21 | bar: number;
22 | }
23 |
24 | /** Docs for FooClass */
25 | class FooClass extends FooInterface {
26 | bar;
27 | constructor() {
28 | this.bar = 1;
29 | }
30 | }
31 |
32 | /** Docs for FooEnum */
33 | enum FooEnum {
34 | Bar,
35 | Baz
36 | }
37 | `,
38 | );
39 | expect(headText(indexFile.getInterfaceOrThrow("FooInterface"))).toBe("interface FooInterface {}");
40 | expect(headText(indexFile.getClassOrThrow("FooClass"))).toBe(
41 | "class FooClass extends FooInterface {}",
42 | );
43 | expect(headText(indexFile.getEnumOrThrow("FooEnum"))).toBe("enum FooEnum {}");
44 | });
45 |
--------------------------------------------------------------------------------
/src/head-text.ts:
--------------------------------------------------------------------------------
1 | import {
2 | EnumDeclaration,
3 | SyntaxKind,
4 | type ClassDeclaration,
5 | type InterfaceDeclaration,
6 | } from "ts-morph";
7 |
8 | export const headText = (
9 | declaration: ClassDeclaration | InterfaceDeclaration | EnumDeclaration,
10 | ): string => {
11 | const parts = [];
12 | for (const node of declaration.getChildren()) {
13 | if (node.getKind() === SyntaxKind.JSDoc) {
14 | // Ignore JSDoc comments.
15 | continue;
16 | }
17 | if (node.getKind() === SyntaxKind.OpenBraceToken) {
18 | // Stop at body start, marked by the first `{`
19 | // found in a class, interface or enum declaration.
20 | break;
21 | }
22 | parts.push(node.getText());
23 | }
24 | parts.push("{}");
25 | return parts.join(" ");
26 | };
27 |
--------------------------------------------------------------------------------
/src/id.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest";
2 | import { id } from "./id";
3 |
4 | test("no parts", () => {
5 | expect(id()).toBe("");
6 | });
7 |
8 | test("one part", () => {
9 | expect(id("foo")).toBe("foo");
10 | });
11 |
12 | test("multiple parts", () => {
13 | expect(id("foo", "bar", "baz")).toBe("foo.bar.baz");
14 | });
15 |
16 | test("multiple parts and empty parts", () => {
17 | expect(id("foo", "", "bar", "", "baz")).toBe("foo.bar.baz");
18 | });
19 |
--------------------------------------------------------------------------------
/src/id.ts:
--------------------------------------------------------------------------------
1 | export const id = (...parts: string[]) => parts.filter(Boolean).join(".");
2 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export type {
2 | AllExtractedDeclaration,
3 | AllExtractedDeclarationKind,
4 | } from "./all-extracted-declaration";
5 | export { bunPackageManager } from "./bun-package-manager";
6 | export { ProjectError } from "./create-project";
7 | export type {
8 | ExtractedClass,
9 | ExtractedClassConstructor,
10 | ExtractedClassMethod,
11 | ExtractedClassProperty,
12 | } from "./extract-class";
13 | export {
14 | extractDeclarations,
15 | type ExtractDeclarationsOptions,
16 | type ExtractedDeclaration,
17 | type ExtractedDeclarationKind,
18 | } from "./extract-declarations";
19 | export type { ExtractedEnum, ExtractedEnumMember } from "./extract-enum";
20 | export type { ExtractedFunction } from "./extract-function";
21 | export type {
22 | ExtractedInterface,
23 | ExtractedInterfaceCallSignature,
24 | ExtractedInterfaceConstructSignature,
25 | ExtractedInterfaceGetAccessor,
26 | ExtractedInterfaceIndexSignature,
27 | ExtractedInterfaceMethod,
28 | ExtractedInterfaceProperty,
29 | ExtractedInterfaceSetAccessor,
30 | } from "./extract-interface";
31 | export type { ExtractedNamespace } from "./extract-namespace";
32 | export {
33 | extractPackageApi,
34 | type ExtractPackageApiOptions,
35 | type PackageApi,
36 | } from "./extract-package-api";
37 | export { extractPackageApiEffect } from "./extract-package-api-effect";
38 | export type { ExtractedTypeAlias } from "./extract-type-alias";
39 | export type { ExtractedVariable } from "./extract-variable";
40 | export { PackageDeclarationsError } from "./package-declarations";
41 | export { packageJson, PackageJsonError } from "./package-json";
42 | export { InstallPackageError, PackageManager, type InstallPackageOptions } from "./package-manager";
43 | export { packageName, PackageNameError } from "./package-name";
44 | export { packageTypes, PackageTypesError } from "./package-types";
45 | export { parseDocComment } from "./parse-doc-comment";
46 | export { workDir, WorkDirError, type WorkDir } from "./work-dir";
47 |
--------------------------------------------------------------------------------
/src/is-class.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { isClass } from "./is-class";
5 |
6 | test("is class", () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | export function foo() {}
20 |
21 | export class Foo {}
22 | `,
23 | );
24 | expect(isClass(indexFile.getFunctionOrThrow("foo"))).toBe(false);
25 | expect(isClass(indexFile.getClassOrThrow("Foo"))).toBe(true);
26 | });
27 |
--------------------------------------------------------------------------------
/src/is-class.ts:
--------------------------------------------------------------------------------
1 | import { Node, type ClassDeclaration } from "ts-morph";
2 |
3 | export const isClass = (node: Node): node is ClassDeclaration => Node.isClassDeclaration(node);
4 |
--------------------------------------------------------------------------------
/src/is-enum.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { isEnum } from "./is-enum";
5 |
6 | test("is enum", () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | export function foo() {}
20 |
21 | export enum Foo {}
22 | `,
23 | );
24 | expect(isEnum(indexFile.getFunctionOrThrow("foo"))).toBe(false);
25 | expect(isEnum(indexFile.getEnumOrThrow("Foo"))).toBe(true);
26 | });
27 |
--------------------------------------------------------------------------------
/src/is-enum.ts:
--------------------------------------------------------------------------------
1 | import { Node, type EnumDeclaration } from "ts-morph";
2 |
3 | export const isEnum = (node: Node): node is EnumDeclaration => Node.isEnumDeclaration(node);
4 |
--------------------------------------------------------------------------------
/src/is-exported-declarations.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { isExportedDeclarations } from "./is-exported-declarations";
5 |
6 | test("is exported declarations", () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | project.createSourceFile(
17 | "file-module.ts",
18 | dedent`
19 | export {};
20 | `,
21 | );
22 | const indexFile = project.createSourceFile(
23 | "index.ts",
24 | dedent`
25 | export const fooVar: string;
26 | export function fooFunc() {}
27 | export class FooClass {}
28 | export interface FooInterface {}
29 | export enum FooEnum {}
30 | export type FooType = {};
31 | export namespace FooNamespace {}
32 | export * as fooModule from './file-module';
33 | export default 42;
34 | `,
35 | );
36 | const exportedDeclarations = indexFile.getExportedDeclarations();
37 | expect(exportedDeclarations.size).toBe(9);
38 | for (const [, declarations] of exportedDeclarations) {
39 | for (const declaration of declarations) {
40 | expect(isExportedDeclarations(declaration)).toBe(true);
41 | }
42 | }
43 | });
44 |
45 | test("arrow function expression", () => {
46 | const project = new Project({
47 | useInMemoryFileSystem: true,
48 | compilerOptions: {
49 | lib: ["lib.esnext.full.d.ts"],
50 | target: ScriptTarget.ESNext,
51 | module: ModuleKind.ESNext,
52 | moduleResolution: ModuleResolutionKind.Bundler,
53 | },
54 | });
55 | const indexFile = project.createSourceFile(
56 | "index.ts",
57 | dedent`
58 | export default () => {}
59 | `,
60 | );
61 | const exportedDeclarations = indexFile.getExportedDeclarations();
62 | expect(exportedDeclarations.size).toBe(1);
63 | for (const [, declarations] of exportedDeclarations) {
64 | for (const declaration of declarations) {
65 | expect(isExportedDeclarations(declaration)).toBe(true);
66 | }
67 | }
68 | });
69 |
--------------------------------------------------------------------------------
/src/is-exported-declarations.ts:
--------------------------------------------------------------------------------
1 | import { Node, type ExportedDeclarations } from "ts-morph";
2 |
3 | export const isExportedDeclarations = (node: Node): node is ExportedDeclarations =>
4 | Node.isVariableDeclaration(node) ||
5 | Node.isFunctionDeclaration(node) ||
6 | Node.isClassDeclaration(node) ||
7 | Node.isInterfaceDeclaration(node) ||
8 | Node.isEnumDeclaration(node) ||
9 | Node.isTypeAliasDeclaration(node) ||
10 | Node.isModuleDeclaration(node) ||
11 | Node.isExpression(node) ||
12 | Node.isSourceFile(node);
13 |
--------------------------------------------------------------------------------
/src/is-expression.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { isExpression } from "./is-expression";
5 |
6 | test("is expression", () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | export function foo() {}
20 |
21 | export default 42;
22 | `,
23 | );
24 | expect(isExpression(indexFile.getFunctionOrThrow("foo"))).toBe(false);
25 | expect(isExpression(indexFile.getExportedDeclarations().get("default")?.at(0)!)).toBe(true);
26 | });
27 |
--------------------------------------------------------------------------------
/src/is-expression.ts:
--------------------------------------------------------------------------------
1 | import { Node, type Expression } from "ts-morph";
2 |
3 | export const isExpression: (node: Node) => node is Expression = (node: Node): node is Expression =>
4 | Node.isExpression(node) && !Node.isArrowFunction(node);
5 |
--------------------------------------------------------------------------------
/src/is-file-module.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { isFileModule } from "./is-file-module";
5 |
6 | test("is file module", () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | export function foo() {}
20 | `,
21 | );
22 | expect(isFileModule(indexFile.getFunctionOrThrow("foo"))).toBe(false);
23 | expect(isFileModule(indexFile)).toBe(true);
24 | });
25 |
--------------------------------------------------------------------------------
/src/is-file-module.ts:
--------------------------------------------------------------------------------
1 | import { Node, type SourceFile } from "ts-morph";
2 |
3 | export const isFileModule = (node: Node): node is SourceFile => Node.isSourceFile(node);
4 |
--------------------------------------------------------------------------------
/src/is-function-expression.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { isFunctionExpression } from "./is-function-expression";
5 |
6 | test("is function expression", () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | export function foo() {}
20 |
21 | export const bar = () => {};
22 | export const baz = function() {};
23 | export const qux: () => void;
24 | `,
25 | );
26 | expect(isFunctionExpression(indexFile.getFunctionOrThrow("foo"))).toBe(false);
27 | expect(isFunctionExpression(indexFile.getVariableDeclarationOrThrow("bar"))).toBe(true);
28 | expect(isFunctionExpression(indexFile.getVariableDeclarationOrThrow("baz"))).toBe(true);
29 | expect(isFunctionExpression(indexFile.getVariableDeclarationOrThrow("qux"))).toBe(true);
30 | });
31 |
--------------------------------------------------------------------------------
/src/is-function-expression.ts:
--------------------------------------------------------------------------------
1 | import { Node, SyntaxKind, type VariableDeclaration } from "ts-morph";
2 |
3 | export const isFunctionExpression = (node: Node): node is VariableDeclaration =>
4 | Node.isVariableDeclaration(node) && hasFunctionLikeType(node);
5 |
6 | const hasFunctionLikeType = (declaration: VariableDeclaration): boolean => {
7 | if (declaration.getTypeNode()?.getKind() === SyntaxKind.FunctionType) {
8 | // Check type signature after `:` (e.g., `const foo: () => void;`).
9 | return true;
10 | }
11 | const initializer = declaration.getInitializer();
12 | if (!initializer) {
13 | return false;
14 | }
15 | return Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer);
16 | };
17 |
--------------------------------------------------------------------------------
/src/is-function.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { isFunction } from "./is-function";
5 |
6 | test("is function", () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | export const foo: string;
20 |
21 | export function bar() {}
22 | `,
23 | );
24 | expect(isFunction(indexFile.getVariableDeclarationOrThrow("foo"))).toBe(false);
25 | expect(isFunction(indexFile.getFunctionOrThrow("bar"))).toBe(true);
26 | });
27 |
--------------------------------------------------------------------------------
/src/is-function.ts:
--------------------------------------------------------------------------------
1 | import { ArrowFunction, FunctionDeclaration, Node } from "ts-morph";
2 |
3 | export const isFunction = (node: Node): node is FunctionDeclaration | ArrowFunction =>
4 | Node.isFunctionDeclaration(node) || Node.isArrowFunction(node);
5 |
--------------------------------------------------------------------------------
/src/is-global.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { isGlobal } from "./is-global";
5 |
6 | test("is global", () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.d.ts",
18 | dedent`
19 | declare var foo: number;
20 | declare const bar: number;
21 | declare function baz(a: number): void;
22 | declare namespace qux {};
23 | `,
24 | );
25 | expect(isGlobal(indexFile.getVariableDeclarationOrThrow("foo"))).toBe(true);
26 | expect(isGlobal(indexFile.getVariableDeclarationOrThrow("bar"))).toBe(true);
27 | expect(isGlobal(indexFile.getFunctionOrThrow("baz"))).toBe(true);
28 | expect(isGlobal(indexFile.getModuleOrThrow("qux"))).toBe(true);
29 | });
30 |
--------------------------------------------------------------------------------
/src/is-global.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Node,
3 | type FunctionDeclaration,
4 | type ModuleDeclaration,
5 | type VariableDeclaration,
6 | } from "ts-morph";
7 |
8 | export const isGlobal = (
9 | node: Node,
10 | ): node is VariableDeclaration | FunctionDeclaration | ModuleDeclaration => {
11 | const isGlobalVariable =
12 | Node.isVariableDeclaration(node) &&
13 | node.getVariableStatementOrThrow().isAmbient() &&
14 | !node.isExported();
15 | const isGlobalFunction =
16 | Node.isFunctionDeclaration(node) &&
17 | node.isAmbient() &&
18 | node.getName() !== undefined &&
19 | !node.isExported();
20 | const isGlobalNamespace =
21 | Node.isModuleDeclaration(node) &&
22 | node.isAmbient() &&
23 | !node.isExported() &&
24 | !node.hasModuleKeyword();
25 | return isGlobalVariable || isGlobalFunction || isGlobalNamespace;
26 | };
27 |
--------------------------------------------------------------------------------
/src/is-hidden.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { isHidden } from "./is-hidden";
5 |
6 | test("private properties", () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | export class Foo {
20 | bar;
21 | #baz;
22 | #qux() {}
23 | static staticBar;
24 | static #staticBaz;
25 | static #staticQux() {}
26 | }
27 | `,
28 | );
29 | const foo = indexFile.getClassOrThrow("Foo");
30 | const bar = foo.getPropertyOrThrow("bar");
31 | const baz = foo.getPropertyOrThrow("#baz");
32 | const qux = foo.getMethodOrThrow("#qux");
33 | expect(isHidden(bar)).toBe(false);
34 | expect(isHidden(baz)).toBe(true);
35 | expect(isHidden(qux)).toBe(true);
36 | const staticBar = foo.getPropertyOrThrow("staticBar");
37 | const staticBaz = foo.getPropertyOrThrow("#staticBaz");
38 | const staticQux = foo.getMethodOrThrow("#staticQux");
39 | expect(isHidden(staticBar)).toBe(false);
40 | expect(isHidden(staticBaz)).toBe(true);
41 | expect(isHidden(staticQux)).toBe(true);
42 | });
43 |
44 | test("private modifier", () => {
45 | const project = new Project({
46 | useInMemoryFileSystem: true,
47 | compilerOptions: {
48 | lib: ["lib.esnext.full.d.ts"],
49 | target: ScriptTarget.ESNext,
50 | module: ModuleKind.ESNext,
51 | moduleResolution: ModuleResolutionKind.Bundler,
52 | },
53 | });
54 | const indexFile = project.createSourceFile(
55 | "index.ts",
56 | dedent`
57 | export class Foo {
58 | bar;
59 | private baz;
60 | private qux() {}
61 |
62 | static staticBar;
63 | private static staticBaz;
64 | static private staticQux() {}
65 | }
66 | `,
67 | );
68 | const foo = indexFile.getClassOrThrow("Foo");
69 | const bar = foo.getPropertyOrThrow("bar");
70 | const baz = foo.getPropertyOrThrow("baz");
71 | const qux = foo.getMethodOrThrow("qux");
72 | expect(isHidden(bar)).toBe(false);
73 | expect(isHidden(baz)).toBe(true);
74 | expect(isHidden(qux)).toBe(true);
75 | const staticBar = foo.getPropertyOrThrow("staticBar");
76 | const staticBaz = foo.getPropertyOrThrow("staticBaz");
77 | const staticQux = foo.getMethodOrThrow("staticQux");
78 | expect(isHidden(staticBar)).toBe(false);
79 | expect(isHidden(staticBaz)).toBe(true);
80 | expect(isHidden(staticQux)).toBe(true);
81 | });
82 |
83 | test("internal tag", () => {
84 | const project = new Project({
85 | useInMemoryFileSystem: true,
86 | compilerOptions: {
87 | lib: ["lib.esnext.full.d.ts"],
88 | target: ScriptTarget.ESNext,
89 | module: ModuleKind.ESNext,
90 | moduleResolution: ModuleResolutionKind.Bundler,
91 | },
92 | });
93 | const indexFile = project.createSourceFile(
94 | "index.ts",
95 | dedent`
96 | export function foo() {}
97 |
98 | /** @internal */
99 | export function bar() {}
100 |
101 | /** @INTERNAL */
102 | export function baz() {}
103 |
104 | /** @see {@link http://example.com | this is not @internal} */
105 | export function qux() {}
106 | `,
107 | );
108 | const foo = indexFile.getFunctionOrThrow("foo");
109 | const bar = indexFile.getFunctionOrThrow("bar");
110 | const baz = indexFile.getFunctionOrThrow("baz");
111 | const qux = indexFile.getFunctionOrThrow("qux");
112 | expect(isHidden(foo)).toBe(false);
113 | expect(isHidden(bar)).toBe(true);
114 | expect(isHidden(baz)).toBe(true);
115 | expect(isHidden(qux)).toBe(false);
116 | });
117 |
--------------------------------------------------------------------------------
/src/is-hidden.ts:
--------------------------------------------------------------------------------
1 | import { Node, SyntaxKind } from "ts-morph";
2 | import { docs } from "./docs";
3 | import { parseDocComment } from "./parse-doc-comment";
4 |
5 | export const isHidden = (node: Node): boolean =>
6 | // Check if a declaration is part of a package's private API.
7 | isPrivateProperty(node) || hasPrivateModifier(node) || hasInternalTag(node);
8 |
9 | const isPrivateProperty = (node: Node): boolean =>
10 | // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_properties.
11 | Node.hasName(node) && Node.isPrivateIdentifier(node.getNameNode());
12 |
13 | const hasPrivateModifier = (node: Node): boolean =>
14 | // See https://www.typescriptlang.org/docs/handbook/2/classes.html#private.
15 | Node.isModifierable(node) && node.hasModifier(SyntaxKind.PrivateKeyword);
16 |
17 | const hasInternalTag = (node: Node): boolean =>
18 | // See https://tsdoc.org/pages/tags/internal.
19 | docs(node).some((doc) => parseDocComment(doc).modifierTagSet.isInternal());
20 |
--------------------------------------------------------------------------------
/src/is-interface.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { isInterface } from "./is-interface";
5 |
6 | test("is interface", () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | export function foo() {}
20 |
21 | export interface Foo {}
22 | `,
23 | );
24 | expect(isInterface(indexFile.getFunctionOrThrow("foo"))).toBe(false);
25 | expect(isInterface(indexFile.getInterfaceOrThrow("Foo"))).toBe(true);
26 | });
27 |
--------------------------------------------------------------------------------
/src/is-interface.ts:
--------------------------------------------------------------------------------
1 | import { Node, type InterfaceDeclaration } from "ts-morph";
2 |
3 | export const isInterface = (node: Node): node is InterfaceDeclaration =>
4 | Node.isInterfaceDeclaration(node);
5 |
--------------------------------------------------------------------------------
/src/is-namespace.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { isNamespace } from "./is-namespace";
5 |
6 | test("is namespace", () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | export function foo() {}
20 |
21 | export namespace Foo {}
22 | `,
23 | );
24 | expect(isNamespace(indexFile.getFunctionOrThrow("foo"))).toBe(false);
25 | expect(isNamespace(indexFile.getModuleOrThrow("Foo"))).toBe(true);
26 | });
27 |
--------------------------------------------------------------------------------
/src/is-namespace.ts:
--------------------------------------------------------------------------------
1 | import { Node, type ModuleDeclaration } from "ts-morph";
2 |
3 | export const isNamespace = (node: Node): node is ModuleDeclaration =>
4 | Node.isModuleDeclaration(node);
5 |
--------------------------------------------------------------------------------
/src/is-shorthand-ambient-module.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { isShorthandAmbientModule } from "./is-shorthand-ambient-module";
5 |
6 | test("is shorthand ambient module", () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | export function foo() {}
20 |
21 | declare module 'bar';
22 | `,
23 | );
24 | expect(isShorthandAmbientModule(indexFile.getFunctionOrThrow("foo"))).toBe(false);
25 | expect(isShorthandAmbientModule(indexFile.getModuleOrThrow("'bar'"))).toBe(true);
26 | });
27 |
--------------------------------------------------------------------------------
/src/is-shorthand-ambient-module.ts:
--------------------------------------------------------------------------------
1 | import { Node, type ModuleDeclaration } from "ts-morph";
2 |
3 | export const isShorthandAmbientModule = (node: Node): node is ModuleDeclaration =>
4 | // Shorthand ambient modules have no body (e.g., `declare module 'foo';`)
5 | // and their name includes the quotes (e.g., name === "'foo'").
6 | Node.isModuleDeclaration(node) && !node.hasBody();
7 |
--------------------------------------------------------------------------------
/src/is-type-alias.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { isTypeAlias } from "./is-type-alias";
5 |
6 | test("is type alias", () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | export function foo() {}
20 |
21 | export type Foo = {};
22 | `,
23 | );
24 | expect(isTypeAlias(indexFile.getFunctionOrThrow("foo"))).toBe(false);
25 | expect(isTypeAlias(indexFile.getTypeAliasOrThrow("Foo"))).toBe(true);
26 | });
27 |
--------------------------------------------------------------------------------
/src/is-type-alias.ts:
--------------------------------------------------------------------------------
1 | import { Node, type TypeAliasDeclaration } from "ts-morph";
2 |
3 | export const isTypeAlias = (node: Node): node is TypeAliasDeclaration =>
4 | Node.isTypeAliasDeclaration(node);
5 |
--------------------------------------------------------------------------------
/src/is-variable-assignment-expression.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { isVariableAssignmentExpression } from "./is-variable-assignment-expression";
5 |
6 | test("is variable assignment expression", () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | export function foo() {}
20 |
21 | let var1;
22 | export default var1 = "var1";
23 | `,
24 | );
25 | expect(isVariableAssignmentExpression(indexFile.getFunctionOrThrow("foo"))).toBe(false);
26 | expect(
27 | isVariableAssignmentExpression(indexFile.getExportedDeclarations().get("default")?.at(0)!),
28 | ).toBe(true);
29 | });
30 |
--------------------------------------------------------------------------------
/src/is-variable-assignment-expression.ts:
--------------------------------------------------------------------------------
1 | import { Node, type BinaryExpression } from "ts-morph";
2 |
3 | export const isVariableAssignmentExpression: (node: Node) => node is BinaryExpression = (
4 | node: Node,
5 | ): node is BinaryExpression => Node.isBinaryExpression(node) && Node.isIdentifier(node.getLeft());
6 |
--------------------------------------------------------------------------------
/src/is-variable.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { isVariable } from "./is-variable";
5 |
6 | test("is variable", () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | export function foo() {}
20 |
21 | // Not a variable because it represents function.
22 | export const bar: () => void;
23 |
24 | export const baz: string;
25 | `,
26 | );
27 | expect(isVariable(indexFile.getFunctionOrThrow("foo"))).toBe(false);
28 | expect(isVariable(indexFile.getVariableDeclarationOrThrow("bar"))).toBe(false);
29 | expect(isVariable(indexFile.getVariableDeclarationOrThrow("baz"))).toBe(true);
30 | });
31 |
--------------------------------------------------------------------------------
/src/is-variable.ts:
--------------------------------------------------------------------------------
1 | import { Node, type VariableDeclaration } from "ts-morph";
2 | import { isFunctionExpression } from "./is-function-expression";
3 |
4 | export const isVariable = (node: Node): node is VariableDeclaration =>
5 | Node.isVariableDeclaration(node) && !isFunctionExpression(node);
6 |
--------------------------------------------------------------------------------
/src/modifiers-text.test.ts:
--------------------------------------------------------------------------------
1 | import { fromPartial } from "@total-typescript/shoehorn";
2 | import { SyntaxKind } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { modifiersText } from "./modifiers-text";
5 |
6 | test("ignore public keyword", async () => {
7 | expect(
8 | modifiersText(
9 | fromPartial({
10 | getModifiers() {
11 | return [
12 | {
13 | getKind() {
14 | return SyntaxKind.PublicKeyword;
15 | },
16 | getText() {
17 | return "public";
18 | },
19 | },
20 | ];
21 | },
22 | }),
23 | ),
24 | ).toBe("");
25 | });
26 |
27 | test("collect and return modifiers", async () => {
28 | expect(
29 | modifiersText(
30 | fromPartial({
31 | getModifiers() {
32 | return [
33 | {
34 | getKind() {
35 | return SyntaxKind.PublicKeyword;
36 | },
37 | getText() {
38 | return "public";
39 | },
40 | },
41 | {
42 | getKind() {
43 | return SyntaxKind.StaticKeyword;
44 | },
45 | getText() {
46 | return "static";
47 | },
48 | },
49 | {
50 | getKind() {
51 | return SyntaxKind.ReadonlyKeyword;
52 | },
53 | getText() {
54 | return "readonly";
55 | },
56 | },
57 | ];
58 | },
59 | }),
60 | ),
61 | ).toBe("static readonly");
62 | });
63 |
--------------------------------------------------------------------------------
/src/modifiers-text.ts:
--------------------------------------------------------------------------------
1 | import { SyntaxKind, type ModifierableNode } from "ts-morph";
2 |
3 | export const modifiersText = (node: ModifierableNode): string =>
4 | node
5 | .getModifiers()
6 | .filter((modifier) => modifier.getKind() !== SyntaxKind.PublicKeyword)
7 | .map((modifier) => modifier.getText())
8 | .join(" ");
9 |
--------------------------------------------------------------------------------
/src/package-declarations.test.ts:
--------------------------------------------------------------------------------
1 | import { Effect } from "effect";
2 | import dedent from "ts-dedent";
3 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
4 | import { afterEach, expect, test, vi } from "vitest";
5 | import { extractDeclarations } from "./extract-declarations";
6 | import { packageDeclarations, type PackageDeclarationsOptions } from "./package-declarations";
7 |
8 | const _packageDeclarations = (options: PackageDeclarationsOptions) =>
9 | Effect.runPromise(packageDeclarations(options));
10 |
11 | vi.mock("./extract-declarations", () => ({
12 | extractDeclarations: vi.fn(),
13 | }));
14 |
15 | afterEach(() => {
16 | vi.clearAllMocks();
17 | });
18 |
19 | test("success", async () => {
20 | const project = new Project({
21 | useInMemoryFileSystem: true,
22 | compilerOptions: {
23 | lib: ["lib.esnext.full.d.ts"],
24 | target: ScriptTarget.ESNext,
25 | module: ModuleKind.ESNext,
26 | moduleResolution: ModuleResolutionKind.Bundler,
27 | },
28 | });
29 | const indexFile = project.createSourceFile(
30 | "index.ts",
31 | dedent`
32 | export {};
33 | `,
34 | );
35 | vi.mocked(extractDeclarations).mockResolvedValue([]);
36 | await expect(
37 | _packageDeclarations({
38 | pkgName: "foo",
39 | project,
40 | indexFile,
41 | maxDepth: 5,
42 | }),
43 | ).resolves.toStrictEqual([]);
44 | });
45 |
46 | test("failure", async () => {
47 | const project = new Project({
48 | useInMemoryFileSystem: true,
49 | compilerOptions: {
50 | lib: ["lib.esnext.full.d.ts"],
51 | target: ScriptTarget.ESNext,
52 | module: ModuleKind.ESNext,
53 | moduleResolution: ModuleResolutionKind.Bundler,
54 | },
55 | });
56 | const indexFile = project.createSourceFile(
57 | "index.ts",
58 | dedent`
59 | export {};
60 | `,
61 | );
62 | vi.mocked(extractDeclarations).mockRejectedValue(new Error("test"));
63 | await expect(
64 | _packageDeclarations({
65 | pkgName: "foo",
66 | project,
67 | indexFile,
68 | maxDepth: 5,
69 | }),
70 | ).rejects.toThrow();
71 | });
72 |
--------------------------------------------------------------------------------
/src/package-declarations.ts:
--------------------------------------------------------------------------------
1 | import { Data, Effect } from "effect";
2 | import type { Project, SourceFile } from "ts-morph";
3 | import { extractDeclarations } from "./extract-declarations";
4 |
5 | export type PackageDeclarationsOptions = {
6 | pkgName: string;
7 | project: Project;
8 | indexFile: SourceFile;
9 | maxDepth: number;
10 | };
11 |
12 | /** @internal */
13 | export class PackageDeclarationsError extends Data.TaggedError("PackageDeclarationsError")<{
14 | cause?: unknown;
15 | }> {}
16 |
17 | export const packageDeclarations = ({
18 | pkgName,
19 | project,
20 | indexFile,
21 | maxDepth,
22 | }: PackageDeclarationsOptions) =>
23 | Effect.tryPromise({
24 | try: () =>
25 | extractDeclarations({
26 | containerName: "",
27 | container: indexFile,
28 | maxDepth,
29 | project,
30 | pkgName,
31 | }),
32 | catch: (e) => new PackageDeclarationsError({ cause: e }),
33 | });
34 |
--------------------------------------------------------------------------------
/src/package-json.test.ts:
--------------------------------------------------------------------------------
1 | import { Effect } from "effect";
2 | import fs from "node:fs/promises";
3 | import { join } from "pathe";
4 | import { temporaryDirectoryTask } from "tempy";
5 | import { expect, test } from "vitest";
6 | import { packageJson } from "./package-json";
7 |
8 | const _packageJson = (pkgDir: string) => Effect.runPromise(packageJson(pkgDir));
9 |
10 | test("no package.json", async () => {
11 | await temporaryDirectoryTask(async (dir) => {
12 | await expect(_packageJson(dir)).rejects.toThrow();
13 | });
14 | });
15 |
16 | test("with empty package.json", async () => {
17 | await temporaryDirectoryTask(async (dir) => {
18 | await fs.writeFile(join(dir, "package.json"), "{}");
19 | await expect(_packageJson(dir)).resolves.toMatchInlineSnapshot(`
20 | {
21 | "_id": "@",
22 | "name": "",
23 | "readme": "ERROR: No README data found!",
24 | "version": "",
25 | }
26 | `);
27 | });
28 | });
29 |
30 | test("with minimal package.json", async () => {
31 | await temporaryDirectoryTask(async (dir) => {
32 | await fs.writeFile(join(dir, "package.json"), '{ "name": "foo", "version": "1.0.0" }');
33 | await expect(_packageJson(dir)).resolves.toMatchInlineSnapshot(`
34 | {
35 | "_id": "foo@1.0.0",
36 | "name": "foo",
37 | "readme": "ERROR: No README data found!",
38 | "version": "1.0.0",
39 | }
40 | `);
41 | });
42 | });
43 |
44 | test("with minimal scoped package.json", async () => {
45 | await temporaryDirectoryTask(async (dir) => {
46 | await fs.writeFile(join(dir, "package.json"), '{ "name": "@foo/bar", "version": "1.0.0" }');
47 | await expect(_packageJson(dir)).resolves.toMatchInlineSnapshot(`
48 | {
49 | "_id": "@foo/bar@1.0.0",
50 | "name": "@foo/bar",
51 | "readme": "ERROR: No README data found!",
52 | "version": "1.0.0",
53 | }
54 | `);
55 | });
56 | });
57 |
58 | test("with package.json from workdir with npm package", async () => {
59 | await temporaryDirectoryTask(async (dir) => {
60 | await fs.writeFile(join(dir, "package.json"), '{ "dependencies": { "foo": "1.0.0" } }');
61 | await expect(_packageJson(dir)).resolves.toMatchInlineSnapshot(`
62 | {
63 | "_id": "@",
64 | "dependencies": {
65 | "foo": "1.0.0",
66 | },
67 | "name": "",
68 | "readme": "ERROR: No README data found!",
69 | "version": "",
70 | }
71 | `);
72 | });
73 | });
74 |
75 | test("with package.json from workdir with scoped npm package", async () => {
76 | await temporaryDirectoryTask(async (dir) => {
77 | await fs.writeFile(join(dir, "package.json"), '{ "dependencies": { "@foo/bar": "^1.0.0" } }');
78 | await expect(_packageJson(dir)).resolves.toMatchInlineSnapshot(`
79 | {
80 | "_id": "@",
81 | "dependencies": {
82 | "@foo/bar": "^1.0.0",
83 | },
84 | "name": "",
85 | "readme": "ERROR: No README data found!",
86 | "version": "",
87 | }
88 | `);
89 | });
90 | });
91 |
92 | test("with package.json from workdir with local package", async () => {
93 | await temporaryDirectoryTask(async (dir) => {
94 | await fs.writeFile(
95 | join(dir, "package.json"),
96 | '{ "dependencies": { "foo": "/path/to/tarball.tgz" } }',
97 | );
98 | await expect(_packageJson(dir)).resolves.toMatchInlineSnapshot(`
99 | {
100 | "_id": "@",
101 | "dependencies": {
102 | "foo": "/path/to/tarball.tgz",
103 | },
104 | "name": "",
105 | "readme": "ERROR: No README data found!",
106 | "version": "",
107 | }
108 | `);
109 | });
110 | });
111 |
112 | test("with package.json from workdir with local scoped package", async () => {
113 | await temporaryDirectoryTask(async (dir) => {
114 | await fs.writeFile(
115 | join(dir, "package.json"),
116 | '{ "dependencies": { "@foo/bar": "/path/to/tarball.tgz" } }',
117 | );
118 | await expect(_packageJson(dir)).resolves.toMatchInlineSnapshot(`
119 | {
120 | "_id": "@",
121 | "dependencies": {
122 | "@foo/bar": "/path/to/tarball.tgz",
123 | },
124 | "name": "",
125 | "readme": "ERROR: No README data found!",
126 | "version": "",
127 | }
128 | `);
129 | });
130 | });
131 |
--------------------------------------------------------------------------------
/src/package-json.ts:
--------------------------------------------------------------------------------
1 | import { Data, Effect } from "effect";
2 | import { readPackage } from "read-pkg";
3 |
4 | /** @internal */
5 | export class PackageJsonError extends Data.TaggedError("PackageJsonError")<{
6 | cause?: unknown;
7 | }> {}
8 |
9 | /** @internal */
10 | export const packageJson = (pkgDir: string) =>
11 | Effect.tryPromise({
12 | try: () => readPackage({ cwd: pkgDir }),
13 | catch: (e) => new PackageJsonError({ cause: e }),
14 | });
15 |
--------------------------------------------------------------------------------
/src/package-manager.ts:
--------------------------------------------------------------------------------
1 | import { Context, Data, Effect } from "effect";
2 |
3 | /** @internal */
4 | export type InstallPackageOptions = {
5 | pkg: string;
6 | cwd: string;
7 | };
8 |
9 | /** @internal */
10 | export class InstallPackageError extends Data.TaggedError("InstallPackageError")<{
11 | cause?: unknown;
12 | }> {}
13 |
14 | /** @internal */
15 | export class PackageManager extends Context.Tag("PackageManager")<
16 | PackageManager,
17 | {
18 | readonly installPackage: ({
19 | pkg,
20 | cwd,
21 | }: InstallPackageOptions) => Effect.Effect;
22 | }
23 | >() {}
24 |
--------------------------------------------------------------------------------
/src/package-name.test.ts:
--------------------------------------------------------------------------------
1 | import { Effect } from "effect";
2 | import { expect, test } from "vitest";
3 | import { packageName } from "./package-name";
4 |
5 | const _packageName = (pkg: string) => Effect.runPromise(packageName(pkg));
6 |
7 | test("no name", async () => {
8 | await expect(_packageName("")).rejects.toThrow();
9 | });
10 |
11 | test("whitespace", async () => {
12 | await expect(_packageName(" ")).rejects.toThrow();
13 | });
14 |
15 | test("at signs", async () => {
16 | await expect(_packageName("@")).rejects.toThrow();
17 | await expect(_packageName("@@")).rejects.toThrow();
18 | await expect(_packageName("@@@")).rejects.toThrow();
19 | });
20 |
21 | test("no name with version", async () => {
22 | await expect(_packageName("@1.0.0")).rejects.toThrow();
23 | });
24 |
25 | test("invalid name", async () => {
26 | await expect(_packageName("!")).rejects.toThrow();
27 | });
28 |
29 | test("invalid name with version", async () => {
30 | await expect(_packageName("!@1.0.0")).rejects.toThrow();
31 | });
32 |
33 | test("bare package name", async () => {
34 | await expect(_packageName("foo")).resolves.toBe("foo");
35 | });
36 |
37 | test("bare package name with version", async () => {
38 | await expect(_packageName("foo@1.0.0")).resolves.toBe("foo");
39 | });
40 |
41 | test("bare package name with range", async () => {
42 | await expect(_packageName("foo@^1")).resolves.toBe("foo");
43 | });
44 |
45 | test("bare package name with tag", async () => {
46 | await expect(_packageName("foo@latest")).resolves.toBe("foo");
47 | });
48 |
49 | test("scoped package name", async () => {
50 | await expect(_packageName("@foo/bar")).resolves.toBe("@foo/bar");
51 | });
52 |
53 | test("scoped package name with version", async () => {
54 | await expect(_packageName("@foo/bar@1.0.0")).resolves.toBe("@foo/bar");
55 | });
56 |
57 | test("scoped package name with range", async () => {
58 | await expect(_packageName("@foo/bar@^1.0.0")).resolves.toBe("@foo/bar");
59 | });
60 |
61 | test("scoped package name with tag", async () => {
62 | await expect(_packageName("@foo/bar@latest")).resolves.toBe("@foo/bar");
63 | });
64 |
--------------------------------------------------------------------------------
/src/package-name.ts:
--------------------------------------------------------------------------------
1 | import { Data, Effect } from "effect";
2 | import validate from "validate-npm-package-name";
3 |
4 | /** @internal */
5 | export class PackageNameError extends Data.TaggedError("PackageNameError")<{
6 | warnings?: string[];
7 | errors?: string[];
8 | }> {}
9 |
10 | /** @internal */
11 | export const packageName = (pkg: string) =>
12 | Effect.gen(function* () {
13 | const versionMarker = pkg.lastIndexOf("@");
14 | const pkgName = pkg.slice(0, versionMarker > 0 ? versionMarker : undefined);
15 | const { validForNewPackages, warnings, errors } = validate(pkgName);
16 | if (!validForNewPackages) {
17 | return yield* new PackageNameError({ warnings, errors });
18 | }
19 | return pkgName;
20 | });
21 |
--------------------------------------------------------------------------------
/src/package-overview.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { packageOverview } from "./package-overview";
5 |
6 | test("no overview", () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | export {};
20 | `,
21 | );
22 | expect(packageOverview(indexFile)).toBeUndefined();
23 | });
24 |
25 | test("with overview", () => {
26 | const project = new Project({
27 | useInMemoryFileSystem: true,
28 | compilerOptions: {
29 | lib: ["lib.esnext.full.d.ts"],
30 | target: ScriptTarget.ESNext,
31 | module: ModuleKind.ESNext,
32 | moduleResolution: ModuleResolutionKind.Bundler,
33 | },
34 | });
35 | const indexFile = project.createSourceFile(
36 | "index.ts",
37 | dedent`
38 | /**
39 | @packageDocumentation
40 | This is the overview.
41 | */
42 |
43 | export {};
44 | `,
45 | );
46 | expect(packageOverview(indexFile)).toBe(dedent`
47 | /**
48 | @packageDocumentation
49 | This is the overview.
50 | */
51 | `);
52 | });
53 |
--------------------------------------------------------------------------------
/src/package-overview.ts:
--------------------------------------------------------------------------------
1 | import { SourceFile, SyntaxKind } from "ts-morph";
2 |
3 | export const packageOverview = (indexFile: SourceFile): string | undefined => {
4 | return indexFile
5 | .getDescendantsOfKind(SyntaxKind.JSDocTag)
6 | .find((tag) => tag.getTagName() === "packageDocumentation")
7 | ?.getParentIfKind(SyntaxKind.JSDoc)
8 | ?.getText();
9 | };
10 |
--------------------------------------------------------------------------------
/src/package-types.ts:
--------------------------------------------------------------------------------
1 | import { Data, Effect } from "effect";
2 | import type { NormalizedPackageJson } from "read-pkg";
3 | import { exports } from "resolve.exports";
4 |
5 | /** @internal */
6 | export class PackageTypesError extends Data.TaggedError("PackageTypesError") {}
7 |
8 | /**
9 | `packageTypes` resolves the types entrypoint file (e.g., `index.d.ts`).
10 |
11 | @param pkgJson - the contents of `package.json`
12 | @param subpath - the selected subpath from the `exports` property of `package.json`
13 |
14 | @internal
15 | */
16 | export const packageTypes = (pkgJson: Partial, subpath: string) =>
17 | Effect.gen(function* () {
18 | const firstPath = yield* resolveExports(pkgJson, subpath);
19 | if (firstPath && isTypesFile(firstPath)) {
20 | return firstPath;
21 | }
22 | const isRootSubpath = [".", pkgJson.name].includes(subpath);
23 | if (isRootSubpath && pkgJson.types && isTypesFile(pkgJson.types)) {
24 | return pkgJson.types;
25 | }
26 | if (isRootSubpath && pkgJson.typings && isTypesFile(pkgJson.typings)) {
27 | return pkgJson.typings;
28 | }
29 | return yield* new PackageTypesError();
30 | });
31 |
32 | const resolveExports = (pkgJson: Partial, subpath: string) => {
33 | try {
34 | const resolvedPaths =
35 | exports(pkgJson, subpath, {
36 | conditions: ["types", "import", "node"],
37 | unsafe: true,
38 | }) ?? [];
39 | return Effect.succeed(resolvedPaths[0] as string | undefined);
40 | } catch {
41 | return Effect.succeed(undefined);
42 | }
43 | };
44 |
45 | const isTypesFile = (filepath: string): boolean =>
46 | [".d.ts", ".d.mts", ".d.cts"].some((ext) => filepath.endsWith(ext));
47 |
--------------------------------------------------------------------------------
/src/parse-doc-comment.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest";
2 | import { parseDocComment } from "./parse-doc-comment";
3 |
4 | test("parses doc comment", () => {
5 | expect(parseDocComment("/** @internal */").modifierTagSet.isInternal()).toBe(true);
6 | });
7 |
--------------------------------------------------------------------------------
/src/parse-doc-comment.ts:
--------------------------------------------------------------------------------
1 | import { TSDocParser, type DocComment } from "@microsoft/tsdoc";
2 | import memoize from "memoize";
3 |
4 | /**
5 | `parseDocComment` parses a JSDoc comment using `@microsoft/tsdoc`.
6 |
7 | Parsed comments are memoized.
8 |
9 | @param s - the raw string comment
10 |
11 | @internal
12 | */
13 | export const parseDocComment = memoize((s: string): DocComment => {
14 | const parser = new TSDocParser();
15 | return parser.parseString(s).docComment;
16 | });
17 |
--------------------------------------------------------------------------------
/src/source-file-path.test.ts:
--------------------------------------------------------------------------------
1 | import { fromPartial } from "@total-typescript/shoehorn";
2 | import { expect, test } from "vitest";
3 | import { sourceFilePath } from "./source-file-path";
4 |
5 | test("empty path", async () => {
6 | expect(
7 | sourceFilePath(
8 | fromPartial({
9 | getSourceFile() {
10 | return {
11 | getFilePath() {
12 | return "";
13 | },
14 | };
15 | },
16 | }),
17 | ),
18 | ).toBe("");
19 | });
20 |
21 | test("only filename", async () => {
22 | expect(
23 | sourceFilePath(
24 | fromPartial({
25 | getSourceFile() {
26 | return {
27 | getFilePath() {
28 | return "index.ts";
29 | },
30 | };
31 | },
32 | }),
33 | ),
34 | ).toBe("index.ts");
35 | });
36 |
37 | test("dir and filename", async () => {
38 | expect(
39 | sourceFilePath(
40 | fromPartial({
41 | getSourceFile() {
42 | return {
43 | getFilePath() {
44 | return "/src/index.ts";
45 | },
46 | };
47 | },
48 | }),
49 | ),
50 | ).toBe("/src/index.ts");
51 | });
52 |
53 | test("node_modules, dir and filename", async () => {
54 | expect(
55 | sourceFilePath(
56 | fromPartial({
57 | getSourceFile() {
58 | return {
59 | getFilePath() {
60 | return "/node_modules/src/index.ts";
61 | },
62 | };
63 | },
64 | }),
65 | ),
66 | ).toBe("/src/index.ts");
67 | });
68 |
69 | test("temp dir, node_modules, package dir and filename", async () => {
70 | expect(
71 | sourceFilePath(
72 | fromPartial({
73 | getSourceFile() {
74 | return {
75 | getFilePath() {
76 | return "/tmp/some_unique_dir/node_modules/my-package/src/index.ts";
77 | },
78 | };
79 | },
80 | }),
81 | ),
82 | ).toBe("/my-package/src/index.ts");
83 | });
84 |
85 | test("temp dir, node_modules, scoped package dir and filename", async () => {
86 | expect(
87 | sourceFilePath(
88 | fromPartial({
89 | getSourceFile() {
90 | return {
91 | getFilePath() {
92 | return "/tmp/some_unique_dir/node_modules/@my-scope/my-package/src/index.ts";
93 | },
94 | };
95 | },
96 | }),
97 | ),
98 | ).toBe("/@my-scope/my-package/src/index.ts");
99 | });
100 |
--------------------------------------------------------------------------------
/src/source-file-path.ts:
--------------------------------------------------------------------------------
1 | import type { Node } from "ts-morph";
2 |
3 | export const sourceFilePath = (node: Node): string =>
4 | node.getSourceFile().getFilePath().split("node_modules").pop()!;
5 |
--------------------------------------------------------------------------------
/src/type-checker-type.test.ts:
--------------------------------------------------------------------------------
1 | import { fromPartial } from "@total-typescript/shoehorn";
2 | import { expect, test } from "vitest";
3 | import { typeCheckerType } from "./type-checker-type";
4 |
5 | test("return `any` in case of errors", async () => {
6 | expect(
7 | typeCheckerType(
8 | fromPartial({
9 | getProject() {
10 | throw new Error();
11 | },
12 | }),
13 | ),
14 | ).toBe("any");
15 | });
16 |
--------------------------------------------------------------------------------
/src/type-checker-type.ts:
--------------------------------------------------------------------------------
1 | import { TypeFormatFlags, type Node } from "ts-morph";
2 |
3 | export const typeCheckerType = (node: Node): string => {
4 | try {
5 | const typeChecker = node.getProject().getTypeChecker().compilerObject;
6 | const nodeType = typeChecker.getTypeAtLocation(node.compilerNode);
7 | return typeChecker.typeToString(
8 | nodeType,
9 | node.compilerNode,
10 | TypeFormatFlags.NoTruncation | TypeFormatFlags.UseAliasDefinedOutsideCurrentScope,
11 | );
12 | } catch {
13 | return "any";
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/src/work-dir.test.ts:
--------------------------------------------------------------------------------
1 | import { Effect } from "effect";
2 | import { rm } from "node:fs/promises";
3 | import { join } from "pathe";
4 | import { temporaryDirectory, temporaryDirectoryTask } from "tempy";
5 | import { afterEach, expect, test, vi } from "vitest";
6 | import { workDir } from "./work-dir";
7 |
8 | const _workDir = () =>
9 | Effect.runPromise(
10 | Effect.gen(function* () {
11 | const { path } = yield* workDir;
12 | return path;
13 | }).pipe(Effect.scoped),
14 | );
15 |
16 | vi.mock("tempy", async (importOriginal) => {
17 | const actual: any = await importOriginal();
18 | return { ...actual, temporaryDirectory: vi.fn() };
19 | });
20 |
21 | vi.mock("node:fs/promises", async () => {
22 | return { rm: vi.fn() };
23 | });
24 |
25 | afterEach(() => {
26 | vi.clearAllMocks();
27 | });
28 |
29 | test("work dirs", async () => {
30 | await temporaryDirectoryTask(async (dir) => {
31 | const dir1 = join(dir, "dir1");
32 | const dir2 = join(dir, "dir2");
33 | const dir3 = join(dir, "dir3");
34 | vi.mocked(temporaryDirectory)
35 | .mockReturnValueOnce(dir1)
36 | .mockReturnValueOnce(dir2)
37 | .mockReturnValueOnce(dir3)
38 | .mockImplementationOnce(() => {
39 | // Fail acquire on 4th call.
40 | throw new Error("test");
41 | });
42 | vi.mocked(rm)
43 | .mockImplementationOnce(async () => {})
44 | .mockImplementationOnce(async () => {})
45 | .mockImplementationOnce(() => {
46 | // Fail release on 3rd call.
47 | throw new Error("test");
48 | })
49 | .mockImplementationOnce(async () => {});
50 | await expect(_workDir()).resolves.toBe(dir1);
51 | await expect(_workDir()).resolves.toBe(dir2);
52 | await expect(_workDir()).resolves.toBe(dir3);
53 | await expect(_workDir()).rejects.toThrow();
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/src/work-dir.ts:
--------------------------------------------------------------------------------
1 | import { Data, Effect } from "effect";
2 | import { rm } from "node:fs/promises";
3 | import { temporaryDirectory } from "tempy";
4 |
5 | /** @internal */
6 | export type WorkDir = {
7 | readonly path: string;
8 | readonly close: () => Promise;
9 | };
10 |
11 | /** @internal */
12 | export class WorkDirError extends Data.TaggedError("WorkDirError")<{
13 | cause?: unknown;
14 | }> {}
15 |
16 | const acquire = Effect.try({
17 | try: () => {
18 | const path = temporaryDirectory();
19 | return {
20 | path,
21 | close: async () => {
22 | try {
23 | await rm(path, { force: true, recursive: true, maxRetries: 3 });
24 | } catch {}
25 | },
26 | };
27 | },
28 | catch: (e) => new WorkDirError({ cause: e }),
29 | });
30 |
31 | const release = (workDir: WorkDir) => Effect.promise(() => workDir.close());
32 |
33 | /** @internal */
34 | export const workDir = Effect.acquireRelease(acquire, release);
35 |
--------------------------------------------------------------------------------
/tarballs/short-time-ago-3.0.0.tgz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jsdocs-io/extractor/0b6ccab6d9c085169f8c6077de559e1fc71f518d/tarballs/short-time-ago-3.0.0.tgz
--------------------------------------------------------------------------------
/test/declarations/__snapshots__/ambient-modules.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`ambient modules 1`] = `
4 | [
5 | {
6 | "declarations": [
7 | {
8 | "docs": [
9 | "/** Variable var1 */",
10 | ],
11 | "file": "/index.d.ts",
12 | "id": "+namespace."bar".+variable.var1",
13 | "kind": "variable",
14 | "line": 7,
15 | "name": "var1",
16 | "signature": "const var1: string;",
17 | },
18 | {
19 | "docs": [
20 | "/** Variable var2 */",
21 | ],
22 | "file": "/index.d.ts",
23 | "id": "+namespace."bar".+variable.var2",
24 | "kind": "variable",
25 | "line": 10,
26 | "name": "var2",
27 | "signature": "const var2: boolean;",
28 | },
29 | ],
30 | "docs": [
31 | "/** Module 'bar' */",
32 | ],
33 | "file": "/index.d.ts",
34 | "id": "+namespace."bar"",
35 | "kind": "namespace",
36 | "line": 5,
37 | "name": ""bar"",
38 | "signature": "module "bar" {}",
39 | },
40 | {
41 | "declarations": [
42 | {
43 | "docs": [],
44 | "file": "/index.d.ts",
45 | "id": "+namespace."foo bar".+function.default",
46 | "kind": "function",
47 | "line": 15,
48 | "name": "default",
49 | "signature": "default: (a: number, b: number) => number;",
50 | },
51 | ],
52 | "docs": [
53 | "/** Module 'foo bar' */",
54 | ],
55 | "file": "/index.d.ts",
56 | "id": "+namespace."foo bar"",
57 | "kind": "namespace",
58 | "line": 14,
59 | "name": ""foo bar"",
60 | "signature": "module "foo bar" {}",
61 | },
62 | {
63 | "declarations": [],
64 | "docs": [
65 | "/** Module 'foo' */",
66 | ],
67 | "file": "/index.d.ts",
68 | "id": "+namespace."foo"",
69 | "kind": "namespace",
70 | "line": 2,
71 | "name": ""foo"",
72 | "signature": "module "foo" {}",
73 | },
74 | ]
75 | `;
76 |
--------------------------------------------------------------------------------
/test/declarations/__snapshots__/ambient-namespaces.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`ambient namespaces 1`] = `
4 | [
5 | {
6 | "docs": [
7 | "/** Global function foobar */",
8 | ],
9 | "file": "/index.d.ts",
10 | "id": "+function.foobar",
11 | "kind": "function",
12 | "line": 20,
13 | "name": "foobar",
14 | "signature": "foobar: (a: number, b: number) => number;",
15 | },
16 | {
17 | "declarations": [
18 | {
19 | "callSignatures": [],
20 | "constructSignatures": [],
21 | "docs": [
22 | "/** Interface Bar */",
23 | ],
24 | "file": "/index.d.ts",
25 | "getAccessors": [],
26 | "id": "+namespace.Foo.+interface.Bar",
27 | "indexSignatures": [],
28 | "kind": "interface",
29 | "line": 4,
30 | "methods": [],
31 | "name": "Bar",
32 | "properties": [
33 | {
34 | "docs": [
35 | "/** Property qux */",
36 | ],
37 | "file": "/index.d.ts",
38 | "id": "+namespace.Foo.+interface.Bar.+property.qux",
39 | "kind": "interface-property",
40 | "line": 6,
41 | "name": "qux",
42 | "signature": "readonly qux: string;",
43 | },
44 | ],
45 | "setAccessors": [],
46 | "signature": "interface Bar {}",
47 | },
48 | {
49 | "docs": [
50 | "/** Variable baz */",
51 | ],
52 | "file": "/index.d.ts",
53 | "id": "+namespace.Foo.+variable.baz",
54 | "kind": "variable",
55 | "line": 10,
56 | "name": "baz",
57 | "signature": "const baz: boolean;",
58 | },
59 | ],
60 | "docs": [
61 | "/** Ambient namespace Foo */",
62 | ],
63 | "file": "/index.d.ts",
64 | "id": "+namespace.Foo",
65 | "kind": "namespace",
66 | "line": 2,
67 | "name": "Foo",
68 | "signature": "namespace Foo {}",
69 | },
70 | {
71 | "docs": [
72 | "/** Global read-only variable bar */",
73 | ],
74 | "file": "/index.d.ts",
75 | "id": "+variable.bar",
76 | "kind": "variable",
77 | "line": 17,
78 | "name": "bar",
79 | "signature": "const bar: Foo.Bar;",
80 | },
81 | {
82 | "docs": [
83 | "/** Global variable foo */",
84 | ],
85 | "file": "/index.d.ts",
86 | "id": "+variable.foo",
87 | "kind": "variable",
88 | "line": 14,
89 | "name": "foo",
90 | "signature": "var foo: Foo.Bar;",
91 | },
92 | ]
93 | `;
94 |
--------------------------------------------------------------------------------
/test/declarations/__snapshots__/class-with-ambient-constructor.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`class with ambient constructor 1`] = `
4 | [
5 | {
6 | "constructors": [
7 | {
8 | "docs": [],
9 | "file": "/index.d.ts",
10 | "id": "+class.Class1.constructor",
11 | "kind": "class-constructor",
12 | "line": 2,
13 | "name": "constructor",
14 | "signature": "constructor();",
15 | },
16 | ],
17 | "docs": [],
18 | "file": "/index.d.ts",
19 | "id": "+class.Class1",
20 | "kind": "class",
21 | "line": 1,
22 | "methods": [],
23 | "name": "Class1",
24 | "properties": [],
25 | "signature": "class Class1 {}",
26 | },
27 | ]
28 | `;
29 |
--------------------------------------------------------------------------------
/test/declarations/__snapshots__/class-with-overloaded-methods.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`class with overloaded methods 1`] = `
4 | [
5 | {
6 | "constructors": [],
7 | "docs": [],
8 | "file": "/index.d.ts",
9 | "id": "+class.Class1",
10 | "kind": "class",
11 | "line": 1,
12 | "methods": [
13 | {
14 | "docs": [
15 | "/** (A) foo */",
16 | "/** (B) foo */",
17 | ],
18 | "file": "/index.d.ts",
19 | "id": "+class.Class1.+method.foo",
20 | "kind": "class-method",
21 | "line": 3,
22 | "name": "foo",
23 | "signature": "foo: { (a: string): number; (b: number): string };",
24 | },
25 | ],
26 | "name": "Class1",
27 | "properties": [],
28 | "signature": "class Class1 {}",
29 | },
30 | ]
31 | `;
32 |
--------------------------------------------------------------------------------
/test/declarations/__snapshots__/empty-index-file.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`empty index file 1`] = `[]`;
4 |
--------------------------------------------------------------------------------
/test/declarations/__snapshots__/export-all-as-namespace.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`export all as namespace 1`] = `
4 | [
5 | {
6 | "declarations": [
7 | {
8 | "docs": [
9 | "/** Variable foo1 */",
10 | ],
11 | "file": "/foo.d.ts",
12 | "id": "+namespace.foo.+variable.foo1",
13 | "kind": "variable",
14 | "line": 6,
15 | "name": "foo1",
16 | "signature": "const foo1: string;",
17 | },
18 | ],
19 | "docs": [
20 | "/**
21 | * Module Foo
22 | */",
23 | ],
24 | "file": "/foo.d.ts",
25 | "id": "+namespace.foo",
26 | "kind": "namespace",
27 | "line": 6,
28 | "name": "foo",
29 | "signature": "module "foo.d.ts" {}",
30 | },
31 | ]
32 | `;
33 |
--------------------------------------------------------------------------------
/test/declarations/__snapshots__/export-as-namespace.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`export as namespace 1`] = `
4 | [
5 | {
6 | "docs": [
7 | "/** func1 */",
8 | ],
9 | "file": "/index.d.ts",
10 | "id": "+function.func1",
11 | "kind": "function",
12 | "line": 2,
13 | "name": "func1",
14 | "signature": "func1: (a: number, b: number) => number;",
15 | },
16 | ]
17 | `;
18 |
--------------------------------------------------------------------------------
/test/declarations/__snapshots__/export-default-arrow-function.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`export default arrow function 1`] = `
4 | [
5 | {
6 | "docs": [
7 | "/** func1 */",
8 | ],
9 | "file": "/index.ts",
10 | "id": "+function.default",
11 | "kind": "function",
12 | "line": 2,
13 | "name": "default",
14 | "signature": "default: () => string;",
15 | },
16 | ]
17 | `;
18 |
--------------------------------------------------------------------------------
/test/declarations/__snapshots__/export-default-class.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`export default class 1`] = `
4 | [
5 | {
6 | "constructors": [],
7 | "docs": [
8 | "/** Class1 */",
9 | ],
10 | "file": "/index.ts",
11 | "id": "+class.default",
12 | "kind": "class",
13 | "line": 2,
14 | "methods": [],
15 | "name": "default",
16 | "properties": [],
17 | "signature": "class {}",
18 | },
19 | ]
20 | `;
21 |
--------------------------------------------------------------------------------
/test/declarations/__snapshots__/export-default-const.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`export default const 1`] = `
4 | [
5 | {
6 | "docs": [
7 | "/** var1 */",
8 | ],
9 | "file": "/index.ts",
10 | "id": "+variable.default",
11 | "kind": "variable",
12 | "line": 2,
13 | "name": "default",
14 | "signature": "const default: string;",
15 | },
16 | ]
17 | `;
18 |
--------------------------------------------------------------------------------
/test/declarations/__snapshots__/export-default-expression.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`export default expression 1`] = `
4 | [
5 | {
6 | "docs": [
7 | "/** expr1 */",
8 | ],
9 | "file": "/index.ts",
10 | "id": "+variable.default",
11 | "kind": "variable",
12 | "line": 2,
13 | "name": "default",
14 | "signature": "const default: any;",
15 | },
16 | ]
17 | `;
18 |
--------------------------------------------------------------------------------
/test/declarations/__snapshots__/export-default-function.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`export default function 1`] = `
4 | [
5 | {
6 | "docs": [
7 | "/** func1 */",
8 | ],
9 | "file": "/index.ts",
10 | "id": "+function.default",
11 | "kind": "function",
12 | "line": 2,
13 | "name": "default",
14 | "signature": "default: (a: number, b: number) => number;",
15 | },
16 | ]
17 | `;
18 |
--------------------------------------------------------------------------------
/test/declarations/__snapshots__/export-default-let-typed.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`export default let typed 1`] = `
4 | [
5 | {
6 | "docs": [
7 | "/** var1 */",
8 | ],
9 | "file": "/index.ts",
10 | "id": "+variable.default",
11 | "kind": "variable",
12 | "line": 2,
13 | "name": "default",
14 | "signature": "let default: string;",
15 | },
16 | ]
17 | `;
18 |
--------------------------------------------------------------------------------
/test/declarations/__snapshots__/export-default-let.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`export default let 1`] = `
4 | [
5 | {
6 | "docs": [
7 | "/** var1 */",
8 | ],
9 | "file": "/index.ts",
10 | "id": "+variable.default",
11 | "kind": "variable",
12 | "line": 2,
13 | "name": "default",
14 | "signature": "let default: string;",
15 | },
16 | ]
17 | `;
18 |
--------------------------------------------------------------------------------
/test/declarations/__snapshots__/export-default-named-class.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`export default named class 1`] = `
4 | [
5 | {
6 | "constructors": [],
7 | "docs": [
8 | "/** Class1 */",
9 | ],
10 | "file": "/index.ts",
11 | "id": "+class.default",
12 | "kind": "class",
13 | "line": 2,
14 | "methods": [],
15 | "name": "default",
16 | "properties": [],
17 | "signature": "class Class1 {}",
18 | },
19 | ]
20 | `;
21 |
--------------------------------------------------------------------------------
/test/declarations/__snapshots__/export-default-named-function.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`export default named function 1`] = `
4 | [
5 | {
6 | "docs": [
7 | "/** func1 */",
8 | ],
9 | "file": "/index.ts",
10 | "id": "+function.default",
11 | "kind": "function",
12 | "line": 2,
13 | "name": "default",
14 | "signature": "default: (a: number, b: number) => number;",
15 | },
16 | ]
17 | `;
18 |
--------------------------------------------------------------------------------
/test/declarations/__snapshots__/export-default-nested-namespaces.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`export default nested namespaces 1`] = `
4 | [
5 | {
6 | "declarations": [
7 | {
8 | "docs": [
9 | "/** fooFunc doc */",
10 | ],
11 | "file": "/index.d.ts",
12 | "id": "+namespace.default.+function.fooFunc",
13 | "kind": "function",
14 | "line": 10,
15 | "name": "fooFunc",
16 | "signature": "fooFunc: (s: string) => string;",
17 | },
18 | {
19 | "declarations": [
20 | {
21 | "docs": [
22 | "/** foobarFunc doc */",
23 | ],
24 | "file": "/index.d.ts",
25 | "id": "+namespace.default.+namespace.bar.+function.foobarFunc",
26 | "kind": "function",
27 | "line": 16,
28 | "name": "foobarFunc",
29 | "signature": "foobarFunc: (n: number) => number;",
30 | },
31 | {
32 | "declarations": [
33 | {
34 | "docs": [
35 | "/** foobarbazFunc doc */",
36 | ],
37 | "file": "/index.d.ts",
38 | "id": "+namespace.default.+namespace.bar.+namespace.baz.+function.foobarbazFunc",
39 | "kind": "function",
40 | "line": 22,
41 | "name": "foobarbazFunc",
42 | "signature": "foobarbazFunc: (b: boolean) => boolean;",
43 | },
44 | {
45 | "declarations": [
46 | {
47 | "declarations": [],
48 | "docs": [],
49 | "file": "/index.d.ts",
50 | "id": "+namespace.default.+namespace.bar.+namespace.baz.+namespace.qux.+namespace.xyz",
51 | "kind": "namespace",
52 | "line": 26,
53 | "name": "xyz",
54 | "signature": "namespace xyz {}",
55 | },
56 | ],
57 | "docs": [],
58 | "file": "/index.d.ts",
59 | "id": "+namespace.default.+namespace.bar.+namespace.baz.+namespace.qux",
60 | "kind": "namespace",
61 | "line": 26,
62 | "name": "qux",
63 | "signature": "namespace qux {}",
64 | },
65 | ],
66 | "docs": [],
67 | "file": "/index.d.ts",
68 | "id": "+namespace.default.+namespace.bar.+namespace.baz",
69 | "kind": "namespace",
70 | "line": 20,
71 | "name": "baz",
72 | "signature": "namespace baz {}",
73 | },
74 | ],
75 | "docs": [],
76 | "file": "/index.d.ts",
77 | "id": "+namespace.default.+namespace.bar",
78 | "kind": "namespace",
79 | "line": 14,
80 | "name": "bar",
81 | "signature": "namespace bar {}",
82 | },
83 | ],
84 | "docs": [
85 | "/** foo doc */",
86 | ],
87 | "file": "/index.d.ts",
88 | "id": "+namespace.default",
89 | "kind": "namespace",
90 | "line": 8,
91 | "name": "default",
92 | "signature": "namespace default {}",
93 | },
94 | ]
95 | `;
96 |
--------------------------------------------------------------------------------
/test/declarations/__snapshots__/export-default-object.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`export default object 1`] = `
4 | [
5 | {
6 | "docs": [
7 | "/** obj1 */",
8 | ],
9 | "file": "/index.ts",
10 | "id": "+variable.default",
11 | "kind": "variable",
12 | "line": 2,
13 | "name": "default",
14 | "signature": "const default: { a: string; b: boolean; c: number; d: { e: string } };",
15 | },
16 | ]
17 | `;
18 |
--------------------------------------------------------------------------------
/test/declarations/__snapshots__/export-equals-function-and-namespace.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`export equals function and namespace 1`] = `
4 | [
5 | {
6 | "docs": [
7 | "/** func1 function */",
8 | ],
9 | "file": "/index.d.ts",
10 | "id": "+function.func1",
11 | "kind": "function",
12 | "line": 10,
13 | "name": "func1",
14 | "signature": "func1: typeof func1;",
15 | },
16 | {
17 | "callSignatures": [],
18 | "constructSignatures": [],
19 | "docs": [
20 | "/** Interface1 */",
21 | ],
22 | "file": "/index.d.ts",
23 | "getAccessors": [],
24 | "id": "+interface.Interface1",
25 | "indexSignatures": [],
26 | "kind": "interface",
27 | "line": 16,
28 | "methods": [],
29 | "name": "Interface1",
30 | "properties": [
31 | {
32 | "docs": [],
33 | "file": "/index.d.ts",
34 | "id": "+interface.Interface1.+property.foo",
35 | "kind": "interface-property",
36 | "line": 17,
37 | "name": "foo",
38 | "signature": "readonly foo: string;",
39 | },
40 | ],
41 | "setAccessors": [],
42 | "signature": "interface Interface1 {}",
43 | },
44 | {
45 | "callSignatures": [],
46 | "constructSignatures": [],
47 | "docs": [
48 | "/** Interface2 */",
49 | ],
50 | "file": "/index.d.ts",
51 | "getAccessors": [],
52 | "id": "+interface.Interface2",
53 | "indexSignatures": [],
54 | "kind": "interface",
55 | "line": 21,
56 | "methods": [],
57 | "name": "Interface2",
58 | "properties": [
59 | {
60 | "docs": [],
61 | "file": "/index.d.ts",
62 | "id": "+interface.Interface2.+property.bar",
63 | "kind": "interface-property",
64 | "line": 22,
65 | "name": "bar",
66 | "signature": "readonly bar: number;",
67 | },
68 | ],
69 | "setAccessors": [],
70 | "signature": "interface Interface2 {}",
71 | },
72 | {
73 | "docs": [
74 | "/** var1 */",
75 | ],
76 | "file": "/index.d.ts",
77 | "id": "+variable.var1",
78 | "kind": "variable",
79 | "line": 26,
80 | "name": "var1",
81 | "signature": "const var1: string;",
82 | },
83 | ]
84 | `;
85 |
--------------------------------------------------------------------------------
/test/declarations/__snapshots__/export-equals-internal-declaration.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`export equals internal declaration 1`] = `
4 | [
5 | {
6 | "docs": [],
7 | "file": "/index.d.ts",
8 | "id": "+variable._foo",
9 | "kind": "variable",
10 | "line": 1,
11 | "name": "_foo",
12 | "signature": "const _foo: number;",
13 | },
14 | ]
15 | `;
16 |
--------------------------------------------------------------------------------
/test/declarations/__snapshots__/export-named-class-with-private-field.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`export named class with private field 1`] = `
4 | [
5 | {
6 | "constructors": [],
7 | "docs": [
8 | "/** Class1 */",
9 | ],
10 | "file": "/index.ts",
11 | "id": "+class.Class1",
12 | "kind": "class",
13 | "line": 2,
14 | "methods": [
15 | {
16 | "docs": [],
17 | "file": "/index.ts",
18 | "id": "+class.Class1.+method.qux",
19 | "kind": "class-method",
20 | "line": 9,
21 | "name": "qux",
22 | "signature": "qux: () => string;",
23 | },
24 | ],
25 | "name": "Class1",
26 | "properties": [
27 | {
28 | "docs": [],
29 | "file": "/index.ts",
30 | "id": "+class.Class1.+property.bar",
31 | "kind": "class-property",
32 | "line": 7,
33 | "name": "bar",
34 | "signature": "bar: number;",
35 | },
36 | ],
37 | "signature": "class Class1 {}",
38 | },
39 | ]
40 | `;
41 |
--------------------------------------------------------------------------------
/test/declarations/__snapshots__/export-named-declaration-without-jsdoc.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`export named declaration without jsdoc 1`] = `
4 | [
5 | {
6 | "docs": [],
7 | "file": "/index.ts",
8 | "id": "+variable.var1",
9 | "kind": "variable",
10 | "line": 7,
11 | "name": "var1",
12 | "signature": "const var1: number;",
13 | },
14 | ]
15 | `;
16 |
--------------------------------------------------------------------------------
/test/declarations/__snapshots__/export-named-import-all.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`export named import all 1`] = `
4 | [
5 | {
6 | "declarations": [
7 | {
8 | "docs": [],
9 | "file": "/bar.d.ts",
10 | "id": "+namespace.bar.+variable.baz",
11 | "kind": "variable",
12 | "line": 1,
13 | "name": "baz",
14 | "signature": "const baz: boolean;",
15 | },
16 | ],
17 | "docs": [],
18 | "file": "/bar.d.ts",
19 | "id": "+namespace.bar",
20 | "kind": "namespace",
21 | "line": 1,
22 | "name": "bar",
23 | "signature": "module "bar.d.ts" {}",
24 | },
25 | {
26 | "declarations": [
27 | {
28 | "docs": [
29 | "/** Function bar2 */",
30 | ],
31 | "file": "/foo.d.ts",
32 | "id": "+namespace.foo.+function.bar2",
33 | "kind": "function",
34 | "line": 9,
35 | "name": "bar2",
36 | "signature": "bar2: (a: number, b: number) => number;",
37 | },
38 | {
39 | "callSignatures": [],
40 | "constructSignatures": [],
41 | "docs": [
42 | "/** Interface Bar3 */",
43 | ],
44 | "file": "/foo.d.ts",
45 | "getAccessors": [],
46 | "id": "+namespace.foo.+interface.Bar3",
47 | "indexSignatures": [],
48 | "kind": "interface",
49 | "line": 12,
50 | "methods": [],
51 | "name": "Bar3",
52 | "properties": [
53 | {
54 | "docs": [],
55 | "file": "/foo.d.ts",
56 | "id": "+namespace.foo.+interface.Bar3.+property.baz",
57 | "kind": "interface-property",
58 | "line": 13,
59 | "name": "baz",
60 | "signature": "readonly baz: (t: T, u: U) => boolean;",
61 | },
62 | ],
63 | "setAccessors": [],
64 | "signature": "interface Bar3 {}",
65 | },
66 | {
67 | "docs": [
68 | "/** Variable bar1 */",
69 | ],
70 | "file": "/foo.d.ts",
71 | "id": "+namespace.foo.+variable.bar1",
72 | "kind": "variable",
73 | "line": 6,
74 | "name": "bar1",
75 | "signature": "const bar1: string;",
76 | },
77 | ],
78 | "docs": [
79 | "/**
80 | * Foo module
81 | */",
82 | ],
83 | "file": "/foo.d.ts",
84 | "id": "+namespace.foo",
85 | "kind": "namespace",
86 | "line": 6,
87 | "name": "foo",
88 | "signature": "module "foo.d.ts" {}",
89 | },
90 | ]
91 | `;
92 |
--------------------------------------------------------------------------------
/test/declarations/__snapshots__/export-named-type-declarations.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`export named type declarations 1`] = `
4 | [
5 | {
6 | "constructors": [],
7 | "docs": [
8 | "/** Class1 */",
9 | ],
10 | "file": "/index.d.ts",
11 | "id": "+class.Class1",
12 | "kind": "class",
13 | "line": 18,
14 | "methods": [],
15 | "name": "Class1",
16 | "properties": [],
17 | "signature": "class Class1 {}",
18 | },
19 | {
20 | "docs": [
21 | "/** Enum1 */",
22 | ],
23 | "file": "/index.d.ts",
24 | "id": "+enum.Enum1",
25 | "kind": "enum",
26 | "line": 24,
27 | "members": [],
28 | "name": "Enum1",
29 | "signature": "enum Enum1 {}",
30 | },
31 | {
32 | "docs": [
33 | "/** func1 */",
34 | ],
35 | "file": "/index.d.ts",
36 | "id": "+function.func1",
37 | "kind": "function",
38 | "line": 11,
39 | "name": "func1",
40 | "signature": "func1: { (a: number, b: number): number; (a: string, b: string): string };",
41 | },
42 | {
43 | "docs": [
44 | "/** func2 */",
45 | ],
46 | "file": "/index.d.ts",
47 | "id": "+function.func2",
48 | "kind": "function",
49 | "line": 15,
50 | "name": "func2",
51 | "signature": "func2: (a: number, b: number) => number;",
52 | },
53 | {
54 | "callSignatures": [],
55 | "constructSignatures": [],
56 | "docs": [
57 | "/** Interface1 */",
58 | ],
59 | "file": "/index.d.ts",
60 | "getAccessors": [],
61 | "id": "+interface.Interface1",
62 | "indexSignatures": [],
63 | "kind": "interface",
64 | "line": 21,
65 | "methods": [],
66 | "name": "Interface1",
67 | "properties": [],
68 | "setAccessors": [],
69 | "signature": "interface Interface1 {}",
70 | },
71 | {
72 | "declarations": [],
73 | "docs": [
74 | "/** Namespace1 */",
75 | ],
76 | "file": "/index.d.ts",
77 | "id": "+namespace.Namespace1",
78 | "kind": "namespace",
79 | "line": 30,
80 | "name": "Namespace1",
81 | "signature": "namespace Namespace1 {}",
82 | },
83 | {
84 | "docs": [
85 | "/** TypeAlias1 */",
86 | ],
87 | "file": "/index.d.ts",
88 | "id": "+type.TypeAlias1",
89 | "kind": "type",
90 | "line": 27,
91 | "name": "TypeAlias1",
92 | "signature": "type TypeAlias1 = number | string;",
93 | },
94 | {
95 | "docs": [
96 | "/** var1 */",
97 | ],
98 | "file": "/index.d.ts",
99 | "id": "+variable.var1",
100 | "kind": "variable",
101 | "line": 8,
102 | "name": "var1",
103 | "signature": "const var1: string;",
104 | },
105 | ]
106 | `;
107 |
--------------------------------------------------------------------------------
/test/declarations/__snapshots__/export-type-import-type.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`export type import type 1`] = `
4 | [
5 | {
6 | "declarations": [
7 | {
8 | "docs": [
9 | "/** Type alias Foo1 */",
10 | ],
11 | "file": "/foo.d.ts",
12 | "id": "+namespace.Foo.+type.Foo1",
13 | "kind": "type",
14 | "line": 6,
15 | "name": "Foo1",
16 | "signature": "type Foo1 = string | number;",
17 | },
18 | {
19 | "docs": [
20 | "/** Type alias Foo2 */",
21 | ],
22 | "file": "/foo.d.ts",
23 | "id": "+namespace.Foo.+type.Foo2",
24 | "kind": "type",
25 | "line": 9,
26 | "name": "Foo2",
27 | "signature": "type Foo2 = {
28 | bar: boolean;
29 | };",
30 | },
31 | ],
32 | "docs": [
33 | "/**
34 | * Module Foo.
35 | */",
36 | ],
37 | "file": "/foo.d.ts",
38 | "id": "+namespace.Foo",
39 | "kind": "namespace",
40 | "line": 6,
41 | "name": "Foo",
42 | "signature": "module "foo.d.ts" {}",
43 | },
44 | ]
45 | `;
46 |
--------------------------------------------------------------------------------
/test/declarations/__snapshots__/export-variable-and-type-with-identical-name.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`export variable and type with identical name 1`] = `
4 | [
5 | {
6 | "docs": [],
7 | "file": "/index.ts",
8 | "id": "+type.foo",
9 | "kind": "type",
10 | "line": 2,
11 | "name": "foo",
12 | "signature": "type foo = string | number;",
13 | },
14 | {
15 | "docs": [],
16 | "file": "/index.ts",
17 | "id": "+variable.foo",
18 | "kind": "variable",
19 | "line": 3,
20 | "name": "foo",
21 | "signature": "const foo: { a: number };",
22 | },
23 | ]
24 | `;
25 |
--------------------------------------------------------------------------------
/test/declarations/__snapshots__/index-file-with-overview.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`index file with overview 1`] = `[]`;
4 |
--------------------------------------------------------------------------------
/test/declarations/__snapshots__/interface-with-overloaded-methods.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`interface with overloaded methods 1`] = `
4 | [
5 | {
6 | "callSignatures": [],
7 | "constructSignatures": [],
8 | "docs": [],
9 | "file": "/index.d.ts",
10 | "getAccessors": [],
11 | "id": "+interface.Interface1",
12 | "indexSignatures": [],
13 | "kind": "interface",
14 | "line": 1,
15 | "methods": [
16 | {
17 | "docs": [
18 | "/** (A) foo */",
19 | "/** (B) foo */",
20 | ],
21 | "file": "/index.d.ts",
22 | "id": "+interface.Interface1.+method.foo",
23 | "kind": "interface-method",
24 | "line": 3,
25 | "name": "foo",
26 | "signature": "foo: { (a: string): number; (b: number): string };",
27 | },
28 | ],
29 | "name": "Interface1",
30 | "properties": [],
31 | "setAccessors": [],
32 | "signature": "interface Interface1 {}",
33 | },
34 | ]
35 | `;
36 |
--------------------------------------------------------------------------------
/test/declarations/__snapshots__/overloaded-function-with-multiple-docs.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`overloaded function with multiple docs 1`] = `
4 | [
5 | {
6 | "docs": [
7 | "/** (A) number to string */",
8 | "/** (B) string to number */",
9 | ],
10 | "file": "/index.d.ts",
11 | "id": "+function.func1",
12 | "kind": "function",
13 | "line": 2,
14 | "name": "func1",
15 | "signature": "func1: { (a: number): string; (b: string): number };",
16 | },
17 | ]
18 | `;
19 |
--------------------------------------------------------------------------------
/test/declarations/ambient-modules.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { extractDeclarations } from "../../src";
5 |
6 | test("ambient modules", async () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.d.ts",
18 | dedent`
19 | /** Module 'foo' */
20 | declare module "foo";
21 |
22 | /** Module 'bar' */
23 | declare module "bar" {
24 | /** Variable var1 */
25 | const var1: string;
26 |
27 | /** Variable var2 */
28 | export const var2: boolean;
29 | }
30 |
31 | /** Module 'foo bar' */
32 | declare module "foo bar" {
33 | export default function sum(a: number, b: number): number;
34 | }
35 | `,
36 | );
37 | expect(
38 | await extractDeclarations({
39 | containerName: "",
40 | container: indexFile,
41 | maxDepth: 5,
42 | project,
43 | }),
44 | ).toMatchSnapshot();
45 | });
46 |
--------------------------------------------------------------------------------
/test/declarations/ambient-namespaces.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { extractDeclarations } from "../../src";
5 |
6 | test("ambient namespaces", async () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.d.ts",
18 | dedent`
19 | /** Ambient namespace Foo */
20 | declare namespace Foo {
21 | /** Interface Bar */
22 | export interface Bar {
23 | /** Property qux */
24 | readonly qux: string;
25 | }
26 |
27 | /** Variable baz */
28 | const baz: boolean;
29 | }
30 |
31 | /** Global variable foo */
32 | declare var foo: Foo.Bar;
33 |
34 | /** Global read-only variable bar */
35 | declare const bar: Foo.Bar;
36 |
37 | /** Global function foobar */
38 | declare function foobar(a: number, b: number): number;
39 | `,
40 | );
41 | expect(
42 | await extractDeclarations({
43 | containerName: "",
44 | container: indexFile,
45 | maxDepth: 5,
46 | project,
47 | }),
48 | ).toMatchSnapshot();
49 | });
50 |
--------------------------------------------------------------------------------
/test/declarations/class-with-ambient-constructor.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { extractDeclarations } from "../../src";
5 |
6 | test("class with ambient constructor", async () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.d.ts",
18 | dedent`
19 | export class Class1 {
20 | constructor();
21 | }
22 | `,
23 | );
24 | expect(
25 | await extractDeclarations({
26 | containerName: "",
27 | container: indexFile,
28 | maxDepth: 5,
29 | project,
30 | }),
31 | ).toMatchSnapshot();
32 | });
33 |
--------------------------------------------------------------------------------
/test/declarations/class-with-overloaded-methods.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { extractDeclarations } from "../../src";
5 |
6 | test("class with overloaded methods", async () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.d.ts",
18 | dedent`
19 | export class Class1 {
20 | /** (A) foo */
21 | foo(a: string): number;
22 |
23 | /** (B) foo */
24 | foo(b: number): string;
25 | }
26 | `,
27 | );
28 | expect(
29 | await extractDeclarations({
30 | containerName: "",
31 | container: indexFile,
32 | maxDepth: 5,
33 | project,
34 | }),
35 | ).toMatchSnapshot();
36 | });
37 |
--------------------------------------------------------------------------------
/test/declarations/empty-index-file.test.ts:
--------------------------------------------------------------------------------
1 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
2 | import { expect, test } from "vitest";
3 | import { extractDeclarations } from "../../src";
4 |
5 | test("empty index file", async () => {
6 | const project = new Project({
7 | useInMemoryFileSystem: true,
8 | compilerOptions: {
9 | lib: ["lib.esnext.full.d.ts"],
10 | target: ScriptTarget.ESNext,
11 | module: ModuleKind.ESNext,
12 | moduleResolution: ModuleResolutionKind.Bundler,
13 | },
14 | });
15 | const indexFile = project.createSourceFile("index.d.ts", "");
16 | expect(
17 | await extractDeclarations({
18 | containerName: "",
19 | container: indexFile,
20 | maxDepth: 5,
21 | project,
22 | }),
23 | ).toMatchSnapshot();
24 | });
25 |
--------------------------------------------------------------------------------
/test/declarations/export-all-as-namespace.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { extractDeclarations } from "../../src";
5 |
6 | test("export all as namespace", async () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | project.createSourceFile(
17 | "foo.d.ts",
18 | dedent`
19 | /**
20 | * Module Foo
21 | */
22 |
23 | /** Variable foo1 */
24 | export const foo1: string;
25 | `,
26 | );
27 | const indexFile = project.createSourceFile(
28 | "index.d.ts",
29 | dedent`
30 | // See https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#export--as-ns-syntax
31 |
32 | // @ts-ignore
33 | export * as external from "this-package-does-not-exist";
34 | export * as foo from "./foo";
35 | `,
36 | );
37 | expect(
38 | await extractDeclarations({
39 | containerName: "",
40 | container: indexFile,
41 | maxDepth: 5,
42 | project,
43 | }),
44 | ).toMatchSnapshot();
45 | });
46 |
--------------------------------------------------------------------------------
/test/declarations/export-as-namespace.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { extractDeclarations } from "../../src";
5 |
6 | test("export as namespace", async () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.d.ts",
18 | dedent`
19 | /** func1 */
20 | export function func1(a: number, b: number): number;
21 |
22 | export as namespace foo;
23 | `,
24 | );
25 | expect(
26 | await extractDeclarations({
27 | containerName: "",
28 | container: indexFile,
29 | maxDepth: 5,
30 | project,
31 | }),
32 | ).toMatchSnapshot();
33 | });
34 |
--------------------------------------------------------------------------------
/test/declarations/export-default-arrow-function.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { extractDeclarations } from "../../src";
5 |
6 | test("export default arrow function", async () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | /** func1 */
20 | export default () => "foo";
21 | `,
22 | );
23 | expect(
24 | await extractDeclarations({
25 | containerName: "",
26 | container: indexFile,
27 | maxDepth: 5,
28 | project,
29 | }),
30 | ).toMatchSnapshot();
31 | });
32 |
--------------------------------------------------------------------------------
/test/declarations/export-default-class.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { extractDeclarations } from "../../src";
5 |
6 | test("export default class", async () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | /** Class1 */
20 | export default class {}
21 | `,
22 | );
23 | expect(
24 | await extractDeclarations({
25 | containerName: "",
26 | container: indexFile,
27 | maxDepth: 5,
28 | project,
29 | }),
30 | ).toMatchSnapshot();
31 | });
32 |
--------------------------------------------------------------------------------
/test/declarations/export-default-const.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { extractDeclarations } from "../../src";
5 |
6 | test("export default const", async () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | /** var1 */
20 | const var1 = "var1";
21 |
22 | export default var1;
23 | `,
24 | );
25 | expect(
26 | await extractDeclarations({
27 | containerName: "",
28 | container: indexFile,
29 | maxDepth: 5,
30 | project,
31 | }),
32 | ).toMatchSnapshot();
33 | });
34 |
--------------------------------------------------------------------------------
/test/declarations/export-default-expression.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { extractDeclarations } from "../../src";
5 |
6 | test("export default expression", async () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | /** expr1 */
20 | export default 42;
21 | `,
22 | );
23 | expect(
24 | await extractDeclarations({
25 | containerName: "",
26 | container: indexFile,
27 | maxDepth: 5,
28 | project,
29 | }),
30 | ).toMatchSnapshot();
31 | });
32 |
--------------------------------------------------------------------------------
/test/declarations/export-default-function.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { extractDeclarations } from "../../src";
5 |
6 | test("export default function", async () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | /** func1 */
20 | export default function (a: number, b: number): number {
21 | return a + b;
22 | }
23 | `,
24 | );
25 | expect(
26 | await extractDeclarations({
27 | containerName: "",
28 | container: indexFile,
29 | maxDepth: 5,
30 | project,
31 | }),
32 | ).toMatchSnapshot();
33 | });
34 |
--------------------------------------------------------------------------------
/test/declarations/export-default-let-typed.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { extractDeclarations } from "../../src";
5 |
6 | test("export default let typed", async () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | /** var1 */
20 | let var1: string;
21 |
22 | export default var1 = "var1";
23 | `,
24 | );
25 | expect(
26 | await extractDeclarations({
27 | containerName: "",
28 | container: indexFile,
29 | maxDepth: 5,
30 | project,
31 | }),
32 | ).toMatchSnapshot();
33 | });
34 |
--------------------------------------------------------------------------------
/test/declarations/export-default-let.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { extractDeclarations } from "../../src";
5 |
6 | test("export default let", async () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | /** var1 */
20 | let var1;
21 |
22 | export default var1 = "var1";
23 | `,
24 | );
25 | expect(
26 | await extractDeclarations({
27 | containerName: "",
28 | container: indexFile,
29 | maxDepth: 5,
30 | project,
31 | }),
32 | ).toMatchSnapshot();
33 | });
34 |
--------------------------------------------------------------------------------
/test/declarations/export-default-named-class.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { extractDeclarations } from "../../src";
5 |
6 | test("export default named class", async () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | /** Class1 */
20 | export default class Class1 {}
21 | `,
22 | );
23 | expect(
24 | await extractDeclarations({
25 | containerName: "",
26 | container: indexFile,
27 | maxDepth: 5,
28 | project,
29 | }),
30 | ).toMatchSnapshot();
31 | });
32 |
--------------------------------------------------------------------------------
/test/declarations/export-default-named-function.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { extractDeclarations } from "../../src";
5 |
6 | test("export default named function", async () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | /** func1 */
20 | export default function func1(a: number, b: number): number {
21 | return a + b;
22 | }
23 | `,
24 | );
25 | expect(
26 | await extractDeclarations({
27 | containerName: "",
28 | container: indexFile,
29 | maxDepth: 5,
30 | project,
31 | }),
32 | ).toMatchSnapshot();
33 | });
34 |
--------------------------------------------------------------------------------
/test/declarations/export-default-nested-namespaces.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { extractDeclarations } from "../../src";
5 |
6 | test("export default nested namespaces", async () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.d.ts",
18 | dedent`
19 | /**
20 | * This is similar to the structure of \`index.d.ts\` for \`firebase@8.2.2\`.
21 | *
22 | * @packageDocumentation
23 | */
24 |
25 | /** foo doc */
26 | declare namespace foo {
27 | /** fooFunc doc */
28 | function fooFunc(s: string): string;
29 | }
30 |
31 | /** foo.bar doc */
32 | declare namespace foo.bar {
33 | /** foobarFunc doc */
34 | function foobarFunc(n: number): number;
35 | }
36 |
37 | /** foo.bar.baz doc */
38 | declare namespace foo.bar.baz {
39 | /** foobarbazFunc doc */
40 | function foobarbazFunc(b: boolean): boolean;
41 | }
42 |
43 | // Namespace \`abc\` is at depth 6 and should not be extracted
44 | declare namespace foo.bar.baz.qux.xyz.abc {}
45 |
46 | export default foo;
47 | `,
48 | );
49 | expect(
50 | await extractDeclarations({
51 | containerName: "",
52 | container: indexFile,
53 | maxDepth: 5,
54 | project,
55 | }),
56 | ).toMatchSnapshot();
57 | });
58 |
--------------------------------------------------------------------------------
/test/declarations/export-default-object.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { extractDeclarations } from "../../src";
5 |
6 | test("export default object", async () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | /** obj1 */
20 | export default { a: "foo", b: true, c: 42, d: { e: "bar" } };
21 | `,
22 | );
23 | expect(
24 | await extractDeclarations({
25 | containerName: "",
26 | container: indexFile,
27 | maxDepth: 5,
28 | project,
29 | }),
30 | ).toMatchSnapshot();
31 | });
32 |
--------------------------------------------------------------------------------
/test/declarations/export-equals-function-and-namespace.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { extractDeclarations } from "../../src";
5 |
6 | test("export equals function and namespace", async () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.d.ts",
18 | dedent`
19 | // See https://www.typescriptlang.org/docs/handbook/declaration-files/templates/module-function-d-ts.html
20 |
21 | /**
22 | * This is the overview.
23 | *
24 | * @packageDocumentation
25 | */
26 |
27 | /** func1 function */
28 | declare function func1(s: string): func1.Interface1;
29 | declare function func1(n: number): func1.Interface2;
30 |
31 | /** func1 namespace */
32 | declare namespace func1 {
33 | /** Interface1 */
34 | interface Interface1 {
35 | readonly foo: string;
36 | }
37 |
38 | /** Interface2 */
39 | interface Interface2 {
40 | readonly bar: number;
41 | }
42 |
43 | /** var1 */
44 | const var1: string;
45 | }
46 |
47 | export = func1;
48 | `,
49 | );
50 | expect(
51 | await extractDeclarations({
52 | containerName: "",
53 | container: indexFile,
54 | maxDepth: 5,
55 | project,
56 | }),
57 | ).toMatchSnapshot();
58 | });
59 |
--------------------------------------------------------------------------------
/test/declarations/export-equals-internal-declaration.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { extractDeclarations } from "../../src";
5 |
6 | test("export equals internal declaration", async () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.d.ts",
18 | dedent`
19 | declare const _foo = 1;
20 | export = _foo;
21 | `,
22 | );
23 | expect(
24 | await extractDeclarations({
25 | containerName: "",
26 | container: indexFile,
27 | maxDepth: 5,
28 | project,
29 | }),
30 | ).toMatchSnapshot();
31 | });
32 |
--------------------------------------------------------------------------------
/test/declarations/export-named-class-with-private-field.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { extractDeclarations } from "../../src";
5 |
6 | test("export named class with private field", async () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | /** Class1 */
20 | export class Class1 {
21 | // ECMAScript Private Field
22 | // See https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#ecmascript-private-fields
23 | #foo: string;
24 |
25 | bar: number;
26 |
27 | qux() {
28 | return "";
29 | }
30 |
31 | /** @internal */
32 | constructor(foo: string, bar: number) {
33 | this.#foo = foo;
34 | this.bar = bar;
35 | }
36 | }
37 | `,
38 | );
39 | expect(
40 | await extractDeclarations({
41 | containerName: "",
42 | container: indexFile,
43 | maxDepth: 5,
44 | project,
45 | }),
46 | ).toMatchSnapshot();
47 | });
48 |
--------------------------------------------------------------------------------
/test/declarations/export-named-declaration-without-jsdoc.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { extractDeclarations } from "../../src";
5 |
6 | test("export named declaration without jsdoc", async () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | /**
20 | * This is the overview and not the documentation for \`var1\`.
21 | *
22 | * @packageDocumentation
23 | */
24 |
25 | export const var1 = 1;
26 | `,
27 | );
28 | expect(
29 | await extractDeclarations({
30 | containerName: "",
31 | container: indexFile,
32 | maxDepth: 5,
33 | project,
34 | }),
35 | ).toMatchSnapshot();
36 | });
37 |
--------------------------------------------------------------------------------
/test/declarations/export-named-import-all.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { extractDeclarations } from "../../src";
5 |
6 | test("export named import all", async () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | project.createSourceFile(
17 | "bar.d.ts",
18 | dedent`
19 | export const baz: boolean;
20 | `,
21 | );
22 | project.createSourceFile(
23 | "foo.d.ts",
24 | dedent`
25 | /**
26 | * Foo module
27 | */
28 |
29 | /** Variable bar1 */
30 | export const bar1: string;
31 |
32 | /** Function bar2 */
33 | export function bar2(a: number, b: number): number;
34 |
35 | /** Interface Bar3 */
36 | export interface Bar3 {
37 | readonly baz: (t: T, u: U) => boolean;
38 | }
39 | `,
40 | );
41 | const indexFile = project.createSourceFile(
42 | "index.d.ts",
43 | dedent`
44 | /**
45 | * This is similar to the structure of \`index.d.ts\` for \`fp-ts@2.9.3\`.
46 | *
47 | * @packageDocumentation
48 | */
49 |
50 | import * as bar from "./bar";
51 | import * as foo from "./foo";
52 |
53 | export { foo, bar };
54 | `,
55 | );
56 | expect(
57 | await extractDeclarations({
58 | containerName: "",
59 | container: indexFile,
60 | maxDepth: 5,
61 | project,
62 | }),
63 | ).toMatchSnapshot();
64 | });
65 |
--------------------------------------------------------------------------------
/test/declarations/export-named-type-declarations.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { extractDeclarations } from "../../src";
5 |
6 | test("export named type declarations", async () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.d.ts",
18 | dedent`
19 | /**
20 | * This is the overview.
21 | *
22 | * @packageDocumentation
23 | */
24 |
25 | /** var1 */
26 | export declare const var1 = "var1";
27 |
28 | /** func1 */
29 | export declare function func1(a: number, b: number): number;
30 | export declare function func1(a: string, b: string): string;
31 |
32 | /** func2 */
33 | export declare const func2: (a: number, b: number) => number;
34 |
35 | /** Class1 */
36 | export declare class Class1 {}
37 |
38 | /** Interface1 */
39 | export declare interface Interface1 {}
40 |
41 | /** Enum1 */
42 | export declare enum Enum1 {}
43 |
44 | /** TypeAlias1 */
45 | export declare type TypeAlias1 = number | string;
46 |
47 | /** Namespace1 */
48 | export declare namespace Namespace1 {}
49 | `,
50 | );
51 | expect(
52 | await extractDeclarations({
53 | containerName: "",
54 | container: indexFile,
55 | maxDepth: 5,
56 | project,
57 | }),
58 | ).toMatchSnapshot();
59 | });
60 |
--------------------------------------------------------------------------------
/test/declarations/export-type-import-type.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { extractDeclarations } from "../../src";
5 |
6 | test("export type import type", async () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | project.createSourceFile(
17 | "foo.d.ts",
18 | dedent`
19 | /**
20 | * Module Foo.
21 | */
22 |
23 | /** Type alias Foo1 */
24 | export declare type Foo1 = string | number;
25 |
26 | /** Type alias Foo2 */
27 | export declare type Foo2 = {
28 | bar: boolean;
29 | };
30 |
31 | export {};
32 | `,
33 | );
34 | const indexFile = project.createSourceFile(
35 | "index.d.ts",
36 | dedent`
37 | /**
38 | * This is similar to the structure of \`index.d.ts\` for \`@jest/types@26.6.2\`.
39 | *
40 | * @packageDocumentation
41 | */
42 |
43 | import type * as Foo from "./foo";
44 |
45 | export type { Foo };
46 | `,
47 | );
48 | expect(
49 | await extractDeclarations({
50 | containerName: "",
51 | container: indexFile,
52 | maxDepth: 5,
53 | project,
54 | }),
55 | ).toMatchSnapshot();
56 | });
57 |
--------------------------------------------------------------------------------
/test/declarations/export-variable-and-type-with-identical-name.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { extractDeclarations } from "../../src";
5 |
6 | test("export variable and type with identical name", async () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | // Similar to usage suggested with zod type inference.
20 | export type foo = string | number
21 | export const foo = { a: 1 };
22 | `,
23 | );
24 | expect(
25 | await extractDeclarations({
26 | containerName: "",
27 | container: indexFile,
28 | maxDepth: 5,
29 | project,
30 | }),
31 | ).toMatchSnapshot();
32 | });
33 |
--------------------------------------------------------------------------------
/test/declarations/index-file-with-overview.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { extractDeclarations } from "../../src";
5 |
6 | test("index file with overview", async () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.ts",
18 | dedent`
19 | /**
20 | * This is the overview.
21 | *
22 | * @packageDocumentation
23 | */
24 | `,
25 | );
26 | expect(
27 | await extractDeclarations({
28 | containerName: "",
29 | container: indexFile,
30 | maxDepth: 5,
31 | project,
32 | }),
33 | ).toMatchSnapshot();
34 | });
35 |
--------------------------------------------------------------------------------
/test/declarations/interface-with-overloaded-methods.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { extractDeclarations } from "../../src";
5 |
6 | test("interface with overloaded methods", async () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.d.ts",
18 | dedent`
19 | export interface Interface1 {
20 | /** (A) foo */
21 | foo(a: string): number;
22 |
23 | /** (B) foo */
24 | foo(b: number): string;
25 | }
26 | `,
27 | );
28 | expect(
29 | await extractDeclarations({
30 | containerName: "",
31 | container: indexFile,
32 | maxDepth: 5,
33 | project,
34 | }),
35 | ).toMatchSnapshot();
36 | });
37 |
--------------------------------------------------------------------------------
/test/declarations/overloaded-function-with-multiple-docs.test.ts:
--------------------------------------------------------------------------------
1 | import dedent from "ts-dedent";
2 | import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from "ts-morph";
3 | import { expect, test } from "vitest";
4 | import { extractDeclarations } from "../../src";
5 |
6 | test("overloaded function with multiple docs", async () => {
7 | const project = new Project({
8 | useInMemoryFileSystem: true,
9 | compilerOptions: {
10 | lib: ["lib.esnext.full.d.ts"],
11 | target: ScriptTarget.ESNext,
12 | module: ModuleKind.ESNext,
13 | moduleResolution: ModuleResolutionKind.Bundler,
14 | },
15 | });
16 | const indexFile = project.createSourceFile(
17 | "index.d.ts",
18 | dedent`
19 | /** (A) number to string */
20 | export function func1(a: number): string;
21 |
22 | /** (B) string to number */
23 | export function func1(b: string): number;
24 | `,
25 | );
26 | expect(
27 | await extractDeclarations({
28 | containerName: "",
29 | container: indexFile,
30 | maxDepth: 5,
31 | project,
32 | }),
33 | ).toMatchSnapshot();
34 | });
35 |
--------------------------------------------------------------------------------
/test/packages/__snapshots__/short-time-ago.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`short-time-ago@2.0.0 1`] = `
4 | {
5 | "analyzedAt": Any,
6 | "analyzedIn": Any,
7 | "declarations": [
8 | {
9 | "docs": [
10 | "/**
11 | * \`timeAgo\` returns a string describing the time elapsed between
12 | * a given date and the current time at which the function is called.
13 | *
14 | * @remarks
15 | * \`timeAgo\` only supports the \`en_US\` locale.
16 | *
17 | * The following table describes \`timeAgo\`'s output.
18 | *
19 | * \`\`\`
20 | * | Time elapsed | Past output | Future output |
21 | * | --------------------- | ----------------- | ---------------- |
22 | * | < 1 second | \`just now\` | \`just now\` |
23 | * | < 1 minute | \`N second(s) ago\` | \`in N second(s)\` |
24 | * | < 1 hour | \`N minute(s) ago\` | \`in N minute(s)\` |
25 | * | < 1 day | \`N hour(s) ago\` | \`in N hour(s)\` |
26 | * | < 1 month (30.5 days) | \`N day(s) ago\` | \`in N day(s)\` |
27 | * | < 1 year (365 days) | \`N month(s) ago\` | \`in N month(s)\` |
28 | * | > 1 year | \`N year(s) ago\` | \`in N year(s)\` |
29 | * \`\`\`
30 | *
31 | * @example
32 | * Basic usage:
33 | *
34 | * \`\`\`typescript
35 | * import { timeAgo } from 'short-time-ago';
36 | *
37 | * const myDate = new Date();
38 | * const description = timeAgo(myDate);
39 | *
40 | * // Output: \`just now\`.
41 | * console.log(description);
42 | * \`\`\`
43 | *
44 | * @example
45 | * Specifying a custom current date with the \`now\` parameter:
46 | *
47 | * \`\`\`typescript
48 | * import { timeAgo } from 'short-time-ago';
49 | *
50 | * const myDate = new Date('2019-01-01T00:00:00.000Z');
51 | * const now = new Date('2019-01-01T00:01:00.000Z');
52 | * const description = timeAgo(myDate, now);
53 | *
54 | * // Output: \`1 minute ago\`.
55 | * console.log(description);
56 | * \`\`\`
57 | *
58 | * \`\`\`typescript
59 | * import { timeAgo } from 'short-time-ago';
60 | *
61 | * const myDate = new Date('2019-01-02T00:00:00.000Z');
62 | * const now = new Date('2019-01-01T00:00:00.000Z');
63 | * const description = timeAgo(myDate, now);
64 | *
65 | * // Output: \`in 1 day\`.
66 | * console.log(description);
67 | * \`\`\`
68 | *
69 | * @param date - a {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date | Date}
70 | * @param now - the current date (optional, defaults to \`new Date()\`)
71 | */",
72 | ],
73 | "file": "/short-time-ago/dist/index.d.ts",
74 | "id": "+function.timeAgo",
75 | "kind": "function",
76 | "line": 70,
77 | "name": "timeAgo",
78 | "signature": "timeAgo: (date: Date, now?: Date) => string;",
79 | },
80 | ],
81 | "name": "short-time-ago",
82 | "overview": "/**
83 | * This package exports a single function, {@link timeAgo},
84 | * which describes the time elapsed between a given date and the current date
85 | * in a human readable format (for example, \`10 minutes ago\`, \`in 3 seconds\`).
86 | *
87 | * @packageDocumentation
88 | */",
89 | "packages": Any,
90 | "subpath": ".",
91 | "types": "./dist/index.d.ts",
92 | "version": "2.0.0",
93 | }
94 | `;
95 |
--------------------------------------------------------------------------------
/test/packages/__snapshots__/violentmonkey__dom.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`@violentmonkey/dom@2.1.5 1`] = `
4 | {
5 | "analyzedAt": Any,
6 | "analyzedIn": Any,
7 | "declarations": [
8 | {
9 | "docs": [],
10 | "file": "/@gera2ld/jsx-dom/types/h.d.ts",
11 | "id": "+function.createElement",
12 | "kind": "function",
13 | "line": 6,
14 | "name": "createElement",
15 | "signature": "createElement: (
16 | type: string | VFunction,
17 | props: VProps,
18 | ...children: VChildren[]
19 | ) => VNode;",
20 | },
21 | {
22 | "docs": [],
23 | "file": "/@gera2ld/jsx-dom/types/h.d.ts",
24 | "id": "+function.Fragment",
25 | "kind": "function",
26 | "line": 9,
27 | "name": "Fragment",
28 | "signature": "Fragment: (props: VProps) => VChildren;",
29 | },
30 | {
31 | "docs": [
32 | "/**
33 | * Return all elements that match the given \`xpath\` as an array.
34 | */",
35 | ],
36 | "file": "/@violentmonkey/dom/types/index.d.ts",
37 | "id": "+function.getElementsByXPath",
38 | "kind": "function",
39 | "line": 8,
40 | "name": "getElementsByXPath",
41 | "signature": "getElementsByXPath: (xpath: string, context?: Node) => Node[];",
42 | },
43 | {
44 | "docs": [
45 | "/**
46 | * Walk a node tree and return all text contents in an array.
47 | */",
48 | ],
49 | "file": "/@violentmonkey/dom/types/index.d.ts",
50 | "id": "+function.getTextValues",
51 | "kind": "function",
52 | "line": 12,
53 | "name": "getTextValues",
54 | "signature": "getTextValues: (node: HTMLElement) => any;",
55 | },
56 | {
57 | "docs": [],
58 | "file": "/@gera2ld/jsx-dom/types/h.d.ts",
59 | "id": "+function.h",
60 | "kind": "function",
61 | "line": 6,
62 | "name": "h",
63 | "signature": "h: (
64 | type: string | VFunction,
65 | props: VProps,
66 | ...children: VChildren[]
67 | ) => VNode;",
68 | },
69 | {
70 | "docs": [
71 | "/**
72 | * Render and mount without returning VirtualDOM, useful when you don't need SVG support.
73 | */",
74 | ],
75 | "file": "/@gera2ld/jsx-dom/types/mount.d.ts",
76 | "id": "+function.hm",
77 | "kind": "function",
78 | "line": 13,
79 | "name": "hm",
80 | "signature": "hm: (
81 | type: string | VFunction,
82 | props: VProps,
83 | ...children: VChildren[]
84 | ) => Node;",
85 | },
86 | {
87 | "docs": [
88 | "/**
89 | * Mount vdom as real DOM nodes.
90 | */",
91 | ],
92 | "file": "/@gera2ld/jsx-dom/types/mount.d.ts",
93 | "id": "+function.m",
94 | "kind": "function",
95 | "line": 9,
96 | "name": "m",
97 | "signature": "m: (vnode: VChild) => Node;",
98 | },
99 | {
100 | "docs": [
101 | "/**
102 | * Mount vdom as real DOM nodes.
103 | */",
104 | ],
105 | "file": "/@gera2ld/jsx-dom/types/mount.d.ts",
106 | "id": "+function.mountDom",
107 | "kind": "function",
108 | "line": 9,
109 | "name": "mountDom",
110 | "signature": "mountDom: (vnode: VChild) => Node;",
111 | },
112 | {
113 | "docs": [
114 | "/**
115 | * Observe an existing \`node\` until \`callback\` returns \`true\`.
116 | * The returned function can be called explicitly to disconnect the observer.
117 | *
118 | * \`\`\`js
119 | * VM.observe(document.body, () => {
120 | * const node = document.querySelector('.profile');
121 | * if (node) {
122 | * console.log('It\\'s there!');
123 | * return true;
124 | * }
125 | * });
126 | * \`\`\`
127 | */",
128 | ],
129 | "file": "/@violentmonkey/dom/types/index.d.ts",
130 | "id": "+function.observe",
131 | "kind": "function",
132 | "line": 27,
133 | "name": "observe",
134 | "signature": "observe: (
135 | node: Node,
136 | callback: (
137 | mutations: MutationRecord[],
138 | observer: MutationObserver,
139 | ) => boolean | void,
140 | options?: MutationObserverInit,
141 | ) => () => void;",
142 | },
143 | {
144 | "docs": [],
145 | "file": "/@violentmonkey/dom/types/index.d.ts",
146 | "id": "+variable.versions",
147 | "kind": "variable",
148 | "line": 2,
149 | "name": "versions",
150 | "signature": "const versions: Record & { dom: string };",
151 | },
152 | ],
153 | "name": "@violentmonkey/dom",
154 | "overview": undefined,
155 | "packages": Any,
156 | "subpath": ".",
157 | "types": "types/index.d.ts",
158 | "version": "2.1.5",
159 | }
160 | `;
161 |
--------------------------------------------------------------------------------
/test/packages/__snapshots__/vue-email__nuxt.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`@vue-email/nuxt@0.8.13 1`] = `
4 | {
5 | "analyzedAt": Any,
6 | "analyzedIn": Any,
7 | "declarations": [
8 | {
9 | "callSignatures": [],
10 | "constructSignatures": [],
11 | "docs": [],
12 | "file": "/@vue-email/nuxt/dist/module.d.ts",
13 | "getAccessors": [],
14 | "id": "+interface.ModuleOptions",
15 | "indexSignatures": [],
16 | "kind": "interface",
17 | "line": 4,
18 | "methods": [],
19 | "name": "ModuleOptions",
20 | "properties": [
21 | {
22 | "docs": [],
23 | "file": "/@vue-email/nuxt/dist/module.d.ts",
24 | "id": "+interface.ModuleOptions.+property.autoImport",
25 | "kind": "interface-property",
26 | "line": 8,
27 | "name": "autoImport",
28 | "signature": "autoImport?: boolean;",
29 | },
30 | {
31 | "docs": [],
32 | "file": "/@vue-email/nuxt/dist/module.d.ts",
33 | "id": "+interface.ModuleOptions.+property.baseUrl",
34 | "kind": "interface-property",
35 | "line": 5,
36 | "name": "baseUrl",
37 | "signature": "baseUrl?: string | null;",
38 | },
39 | {
40 | "docs": [],
41 | "file": "/@vue-email/nuxt/dist/module.d.ts",
42 | "id": "+interface.ModuleOptions.+property.i18n",
43 | "kind": "interface-property",
44 | "line": 6,
45 | "name": "i18n",
46 | "signature": "i18n?: I18n;",
47 | },
48 | {
49 | "docs": [],
50 | "file": "/@vue-email/nuxt/dist/module.d.ts",
51 | "id": "+interface.ModuleOptions.+property.playground",
52 | "kind": "interface-property",
53 | "line": 7,
54 | "name": "playground",
55 | "signature": "playground?: boolean;",
56 | },
57 | {
58 | "docs": [],
59 | "file": "/@vue-email/nuxt/dist/module.d.ts",
60 | "id": "+interface.ModuleOptions.+property.tailwind",
61 | "kind": "interface-property",
62 | "line": 10,
63 | "name": "tailwind",
64 | "signature": "tailwind?: VueEmailPluginOptions["tailwind"];",
65 | },
66 | {
67 | "docs": [],
68 | "file": "/@vue-email/nuxt/dist/module.d.ts",
69 | "id": "+interface.ModuleOptions.+property.useNuxtTailwind",
70 | "kind": "interface-property",
71 | "line": 9,
72 | "name": "useNuxtTailwind",
73 | "signature": "useNuxtTailwind?: boolean;",
74 | },
75 | ],
76 | "setAccessors": [],
77 | "signature": "interface ModuleOptions {}",
78 | },
79 | {
80 | "docs": [],
81 | "file": "/@vue-email/nuxt/dist/module.d.ts",
82 | "id": "+variable.default",
83 | "kind": "variable",
84 | "line": 12,
85 | "name": "default",
86 | "signature": "const default: _nuxt_schema.NuxtModule;",
87 | },
88 | ],
89 | "name": "@vue-email/nuxt",
90 | "overview": undefined,
91 | "packages": Any,
92 | "subpath": ".",
93 | "types": "./dist/types.d.ts",
94 | "version": "0.8.13",
95 | }
96 | `;
97 |
--------------------------------------------------------------------------------
/test/packages/bundle-require.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest";
2 | import { extractPackageApi } from "../../src";
3 |
4 | test("bundle-require@4.0.2", async () => {
5 | expect(
6 | await extractPackageApi({
7 | pkg: "bundle-require@4.0.2",
8 | }),
9 | ).toMatchSnapshot({
10 | analyzedAt: expect.any(String),
11 | analyzedIn: expect.any(Number),
12 | packages: expect.any(Array),
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/test/packages/enttec-open-dmx-usb.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest";
2 | import { extractPackageApi } from "../../src";
3 |
4 | test("enttec-open-dmx-usb@4.0.1", async () => {
5 | expect(
6 | await extractPackageApi({
7 | pkg: "enttec-open-dmx-usb@4.0.1",
8 | }),
9 | ).toMatchSnapshot({
10 | analyzedAt: expect.any(String),
11 | analyzedIn: expect.any(Number),
12 | packages: expect.any(Array),
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/test/packages/exome.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest";
2 | import { extractPackageApi } from "../../src";
3 |
4 | test("exome@2.4.0", async () => {
5 | expect(
6 | await extractPackageApi({
7 | pkg: "exome@2.4.0",
8 | }),
9 | ).toMatchSnapshot({
10 | analyzedAt: expect.any(String),
11 | analyzedIn: expect.any(Number),
12 | packages: expect.any(Array),
13 | });
14 | });
15 |
16 | test("exome@2.4.0@ghost", async () => {
17 | expect(
18 | await extractPackageApi({
19 | pkg: "exome@2.4.0",
20 | subpath: "ghost",
21 | }),
22 | ).toMatchSnapshot({
23 | analyzedAt: expect.any(String),
24 | analyzedIn: expect.any(Number),
25 | packages: expect.any(Array),
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/test/packages/faastjs.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest";
2 | import { extractPackageApi } from "../../src";
3 |
4 | test("faastjs@8.0.64", async () => {
5 | expect(
6 | await extractPackageApi({
7 | pkg: "faastjs@8.0.64",
8 | }),
9 | ).toMatchSnapshot({
10 | analyzedAt: expect.any(String),
11 | analyzedIn: expect.any(Number),
12 | packages: expect.any(Array),
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/test/packages/h3.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest";
2 | import { extractPackageApi } from "../../src";
3 |
4 | test("h3@1.10.1", async () => {
5 | expect(
6 | await extractPackageApi({
7 | pkg: "h3@1.10.1",
8 | }),
9 | ).toMatchSnapshot({
10 | analyzedAt: expect.any(String),
11 | analyzedIn: expect.any(Number),
12 | packages: expect.any(Array),
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/test/packages/highlight-words.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest";
2 | import { extractPackageApi } from "../../src";
3 |
4 | test("highlight-words@1.2.2", async () => {
5 | expect(
6 | await extractPackageApi({
7 | pkg: "highlight-words@1.2.2",
8 | }),
9 | ).toMatchSnapshot({
10 | analyzedAt: expect.any(String),
11 | analyzedIn: expect.any(Number),
12 | packages: expect.any(Array),
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/test/packages/kirklin__eslint-config.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest";
2 | import { extractPackageApi } from "../../src";
3 |
4 | test("@kirklin/eslint-config@2.1.0", async () => {
5 | expect(
6 | await extractPackageApi({
7 | pkg: "@kirklin/eslint-config@2.1.0",
8 | }),
9 | ).toMatchSnapshot({
10 | analyzedAt: expect.any(String),
11 | analyzedIn: expect.any(Number),
12 | packages: expect.any(Array),
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/test/packages/luxass__utils.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest";
2 | import { extractPackageApi } from "../../src";
3 |
4 | test("@luxass/utils@1.1.0", async () => {
5 | expect(
6 | await extractPackageApi({
7 | pkg: "@luxass/utils@1.1.0",
8 | }),
9 | ).toMatchSnapshot({
10 | analyzedAt: expect.any(String),
11 | analyzedIn: expect.any(Number),
12 | packages: expect.any(Array),
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/test/packages/magicast.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest";
2 | import { extractPackageApi } from "../../src";
3 |
4 | test("magicast@0.3.3", async () => {
5 | expect(
6 | await extractPackageApi({
7 | pkg: "magicast@0.3.3",
8 | }),
9 | ).toMatchSnapshot({
10 | analyzedAt: expect.any(String),
11 | analyzedIn: expect.any(Number),
12 | packages: expect.any(Array),
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/test/packages/microsoft__api-extractor.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest";
2 | import { extractPackageApi } from "../../src";
3 |
4 | test("@microsoft/api-extractor@7.13.0", async () => {
5 | expect(
6 | await extractPackageApi({
7 | pkg: "@microsoft/api-extractor@7.13.0",
8 | }),
9 | ).toMatchSnapshot({
10 | analyzedAt: expect.any(String),
11 | analyzedIn: expect.any(Number),
12 | packages: expect.any(Array),
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/test/packages/npm.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest";
2 | import { extractPackageApi } from "../../src";
3 |
4 | test("npm@11.4.0", async () => {
5 | await expect(
6 | extractPackageApi({
7 | pkg: "npm@11.4.0",
8 | }),
9 | ).rejects.toThrow();
10 | });
11 |
12 | test("@types/npm@7.19.3", async () => {
13 | expect(
14 | await extractPackageApi({
15 | pkg: "@types/npm@7.19.3",
16 | }),
17 | ).toMatchSnapshot({
18 | analyzedAt: expect.any(String),
19 | analyzedIn: expect.any(Number),
20 | packages: expect.any(Array),
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/test/packages/preact.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest";
2 | import { extractPackageApi } from "../../src";
3 |
4 | test("preact@10.19.4", async () => {
5 | expect(
6 | await extractPackageApi({
7 | pkg: "preact@10.19.4",
8 | }),
9 | ).toMatchSnapshot({
10 | analyzedAt: expect.any(String),
11 | analyzedIn: expect.any(Number),
12 | packages: expect.any(Array),
13 | });
14 | });
15 |
16 | test("preact@10.19.4@hooks", async () => {
17 | expect(
18 | await extractPackageApi({
19 | pkg: "preact@10.19.4",
20 | subpath: "hooks",
21 | }),
22 | ).toMatchSnapshot({
23 | analyzedAt: expect.any(String),
24 | analyzedIn: expect.any(Number),
25 | packages: expect.any(Array),
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/test/packages/query-registry.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest";
2 | import { extractPackageApi } from "../../src";
3 |
4 | test("query-registry@2.6.0", async () => {
5 | expect(
6 | await extractPackageApi({
7 | pkg: "query-registry@2.6.0",
8 | }),
9 | ).toMatchSnapshot({
10 | analyzedAt: expect.any(String),
11 | analyzedIn: expect.any(Number),
12 | packages: expect.any(Array),
13 | });
14 | });
15 |
16 | test("query-registry@3.0.0", async () => {
17 | expect(
18 | await extractPackageApi({
19 | pkg: "query-registry@3.0.0",
20 | }),
21 | ).toMatchSnapshot({
22 | analyzedAt: expect.any(String),
23 | analyzedIn: expect.any(Number),
24 | packages: expect.any(Array),
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/test/packages/sentry__browser.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest";
2 | import { extractPackageApi } from "../../src";
3 |
4 | test("@sentry/browser@7.100.1", async () => {
5 | expect(
6 | await extractPackageApi({
7 | pkg: "@sentry/browser@7.100.1",
8 | }),
9 | ).toMatchSnapshot({
10 | analyzedAt: expect.any(String),
11 | analyzedIn: expect.any(Number),
12 | packages: expect.any(Array),
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/test/packages/short-time-ago.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest";
2 | import { extractPackageApi } from "../../src";
3 |
4 | test("short-time-ago@2.0.0", async () => {
5 | expect(
6 | await extractPackageApi({
7 | pkg: "short-time-ago@2.0.0",
8 | }),
9 | ).toMatchSnapshot({
10 | analyzedAt: expect.any(String),
11 | analyzedIn: expect.any(Number),
12 | packages: expect.any(Array),
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/test/packages/storylite__storylite.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest";
2 | import { extractPackageApi } from "../../src";
3 |
4 | test("@storylite/storylite@0.14.0", async () => {
5 | expect(
6 | await extractPackageApi({
7 | pkg: "@storylite/storylite@0.14.0",
8 | }),
9 | ).toMatchSnapshot({
10 | analyzedAt: expect.any(String),
11 | analyzedIn: expect.any(Number),
12 | packages: expect.any(Array),
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/test/packages/supeffective__dataset.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest";
2 | import { extractPackageApi } from "../../src";
3 |
4 | test("@supeffective/dataset@2.2.2", async () => {
5 | expect(
6 | await extractPackageApi({
7 | pkg: "@supeffective/dataset@2.2.2",
8 | }),
9 | ).toMatchSnapshot({
10 | analyzedAt: expect.any(String),
11 | analyzedIn: expect.any(Number),
12 | packages: expect.any(Array),
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/test/packages/tinyargs.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest";
2 | import { extractPackageApi } from "../../src";
3 |
4 | test("tinyargs@0.1.4", async () => {
5 | expect(
6 | await extractPackageApi({
7 | pkg: "tinyargs@0.1.4",
8 | }),
9 | ).toMatchSnapshot({
10 | analyzedAt: expect.any(String),
11 | analyzedIn: expect.any(Number),
12 | packages: expect.any(Array),
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/test/packages/ts-api-utils.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest";
2 | import { extractPackageApi } from "../../src";
3 |
4 | test("ts-api-utils@2.1.0", async () => {
5 | expect(
6 | await extractPackageApi({
7 | pkg: "ts-api-utils@2.1.0",
8 | }),
9 | ).toMatchSnapshot({
10 | analyzedAt: expect.any(String),
11 | analyzedIn: expect.any(Number),
12 | packages: expect.any(Array),
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/test/packages/twoslash.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest";
2 | import { extractPackageApi } from "../../src";
3 |
4 | test("twoslash@0.2.1", async () => {
5 | expect(
6 | await extractPackageApi({
7 | pkg: "twoslash@0.2.1",
8 | }),
9 | ).toMatchSnapshot({
10 | analyzedAt: expect.any(String),
11 | analyzedIn: expect.any(Number),
12 | packages: expect.any(Array),
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/test/packages/verify-hcaptcha.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest";
2 | import { extractPackageApi } from "../../src";
3 |
4 | test("verify-hcaptcha@1.0.0", async () => {
5 | expect(
6 | await extractPackageApi({
7 | pkg: "verify-hcaptcha@1.0.0",
8 | }),
9 | ).toMatchSnapshot({
10 | analyzedAt: expect.any(String),
11 | analyzedIn: expect.any(Number),
12 | packages: expect.any(Array),
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/test/packages/violentmonkey__dom.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest";
2 | import { extractPackageApi } from "../../src";
3 |
4 | test("@violentmonkey/dom@2.1.5", async () => {
5 | expect(
6 | await extractPackageApi({
7 | pkg: "@violentmonkey/dom@2.1.5",
8 | }),
9 | ).toMatchSnapshot({
10 | analyzedAt: expect.any(String),
11 | analyzedIn: expect.any(Number),
12 | packages: expect.any(Array),
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/test/packages/violentmonkey__shortcut.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest";
2 | import { extractPackageApi } from "../../src";
3 |
4 | test("@violentmonkey/shortcut@1.4.1", async () => {
5 | expect(
6 | await extractPackageApi({
7 | pkg: "@violentmonkey/shortcut@1.4.1",
8 | }),
9 | ).toMatchSnapshot({
10 | analyzedAt: expect.any(String),
11 | analyzedIn: expect.any(Number),
12 | packages: expect.any(Array),
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/test/packages/vue-email__nuxt.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "vitest";
2 | import { extractPackageApi } from "../../src";
3 |
4 | test("@vue-email/nuxt@0.8.13", async () => {
5 | expect(
6 | await extractPackageApi({
7 | pkg: "@vue-email/nuxt@0.8.13",
8 | }),
9 | ).toMatchSnapshot({
10 | analyzedAt: expect.any(String),
11 | analyzedIn: expect.any(Number),
12 | packages: expect.any(Array),
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | // See https://www.totaltypescript.com/tsconfig-cheat-sheet.
2 | {
3 | "compilerOptions": {
4 | // Base options.
5 | "esModuleInterop": true,
6 | "skipLibCheck": true,
7 | "target": "es2022",
8 | "allowJs": true,
9 | "resolveJsonModule": true,
10 | "moduleDetection": "force",
11 | "isolatedModules": true,
12 | "verbatimModuleSyntax": true,
13 | // Strictness.
14 | "strict": true,
15 | "noUncheckedIndexedAccess": true,
16 | "noImplicitOverride": true,
17 | // Building a library.
18 | "declaration": true,
19 | // Transpiling with a bundler.
20 | "module": "preserve",
21 | "noEmit": true,
22 | // No DOM.
23 | "lib": ["es2022"]
24 | },
25 | "exclude": ["coverage", "dist", "node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "tsup";
2 |
3 | export default defineConfig({
4 | entry: ["src/index.ts"],
5 | format: ["esm"],
6 | dts: true,
7 | clean: true,
8 | });
9 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vitest/config";
2 |
3 | export default defineConfig({
4 | test: {
5 | pool: "threads",
6 | testTimeout: 60_000, // Analyzing npm packages takes time.
7 | coverage: { include: ["src/**"] },
8 | },
9 | });
10 |
--------------------------------------------------------------------------------