├── .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 | [![Build status](https://img.shields.io/github/actions/workflow/status/jsdocs-io/extractor/main.yml?branch=main)](https://github.com/jsdocs-io/extractor/actions/workflows/main.yml?query=workflow%3ACI) 4 | [![Coverage](https://img.shields.io/codecov/c/gh/jsdocs-io/extractor)](https://codecov.io/gh/jsdocs-io/extractor) 5 | [![jsDocs.io](https://img.shields.io/badge/jsDocs.io-reference-blue)](https://www.jsdocs.io/package/@jsdocs-io/extractor) 6 | ![Language](https://img.shields.io/github/languages/top/jsdocs-io/extractor) 7 | [![npm](https://img.shields.io/npm/v/@jsdocs-io/extractor)](https://www.npmjs.com/package/@jsdocs-io/extractor) 8 | [![License](https://img.shields.io/github/license/jsdocs-io/extractor)](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 | --------------------------------------------------------------------------------