├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg ├── post-merge ├── pre-commit └── pre-push ├── .jestrc.js ├── .jsdoc.js ├── .nvmrc ├── .prettierrc ├── .releaserc ├── CHANGELOG.md ├── README.md ├── package-lock.json ├── package.json ├── src ├── constants.js ├── features │ ├── extendTypes.js │ ├── index.js │ ├── modulesOnMemberOf.js │ ├── modulesTypesShortName.js │ ├── removeTaggedBlocks.js │ ├── removeTags.js │ ├── tagsReplacement.js │ ├── tsUtilityTypes.js │ ├── typeOfTypes.js │ └── typedefImports.js ├── index.js └── typedef.js ├── tests ├── .eslintrc ├── features │ ├── extendTypes.test.js │ ├── modulesOnMemberOf.test.js │ ├── modulesTypesShortName.test.js │ ├── removeTaggedBlocks.test.js │ ├── removeTags.test.js │ ├── tagsReplacement.test.js │ ├── tsUtilityTypes.test.js │ ├── typeOfTypes.test.js │ └── typedefImports.test.js └── index.test.js └── utils └── scripts ├── docs ├── lint ├── lint-all ├── prepare ├── test └── todo /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = LF 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .github/* 2 | coverage/* 3 | documentation/* 4 | node_modules/* 5 | docs/* 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "plugins": ["@homer0"], 4 | "extends": [ 5 | "plugin:@homer0/node-with-prettier", 6 | "plugin:@homer0/jsdoc" 7 | ], 8 | "rules": { 9 | "jsdoc/check-tag-names": ["error", { 10 | "definedTags": ["parent", "prettierignore"] 11 | }] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js text eol=lf 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### What does this PR do? 2 | 3 | ### How should it be tested manually? 4 | 5 | ```bash 6 | npm test 7 | ``` 8 | 9 | ### TODOs 10 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release NPM package 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Node.js 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version-file: '.nvmrc' 18 | - name: Install dependencies 19 | env: 20 | HUSKY: 0 21 | run: npm ci 22 | - run: npx semantic-release 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 26 | - name: Generate documentation 27 | run: npm run docs 28 | - name: Deploy documentation 29 | uses: JamesIves/github-pages-deploy-action@4.1.1 30 | with: 31 | BRANCH: gh-pages 32 | FOLDER: docs 33 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node: [ '18', '20' ] 11 | name: Run jest and ESLint (Node ${{ matrix.node }}) 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: ${{ matrix.node }} 17 | - run: npm ci 18 | - run: npm run lint:all 19 | - run: npm test 20 | - name: Coveralls 21 | if: ${{ matrix.node == '18' }} 22 | uses: coverallsapp/github-action@master 23 | with: 24 | github-token: ${{ secrets.GITHUB_TOKEN }} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .DS_Store 4 | npm-debug.log 5 | yarn-error.log 6 | dist 7 | /docs 8 | .vscode 9 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/post-merge: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm install 5 | 6 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint 5 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm test 5 | -------------------------------------------------------------------------------- /.jestrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | automock: true, 3 | clearMocks: true, 4 | collectCoverage: true, 5 | coverageProvider: 'v8', 6 | testPathIgnorePatterns: ['/node_modules/', '/utils/scripts/'], 7 | unmockedModulePathPatterns: ['/node_modules/', '/constants.js/'], 8 | testEnvironment: 'node', 9 | }; 10 | -------------------------------------------------------------------------------- /.jsdoc.js: -------------------------------------------------------------------------------- 1 | const packageJson = require('./package.json'); 2 | 3 | module.exports = { 4 | source: { 5 | include: ['./src'], 6 | includePattern: '.js$', 7 | }, 8 | plugins: ['docdash/nativeTypesPlugin', './src', 'plugins/markdown'], 9 | templates: { 10 | cleverLinks: true, 11 | default: { 12 | includeDate: false, 13 | }, 14 | }, 15 | opts: { 16 | recurse: true, 17 | destination: './docs', 18 | readme: 'README.md', 19 | template: 'node_modules/docdash', 20 | }, 21 | docdash: { 22 | title: packageJson.name, 23 | meta: { 24 | title: `${packageJson.name} docs`, 25 | }, 26 | sectionOrder: ['Classes'], 27 | collapse: true, 28 | refLinks: [ 29 | { 30 | title: 'View the package on Yarn', 31 | url: `https://yarnpkg.com/package/${packageJson.name}`, 32 | type: 'yarn', 33 | }, 34 | { 35 | title: 'Go to the GitHub repository', 36 | url: `https://github.com/${packageJson.repository}`, 37 | type: 'github', 38 | }, 39 | { 40 | title: 'View the package on NPM', 41 | url: `https://www.npmjs.com/package/${packageJson.name}`, 42 | type: 'npm', 43 | }, 44 | ], 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18.17 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | "@homer0/prettier-config" 2 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "tagFormat": "${version}", 3 | "plugins": [ 4 | "@semantic-release/commit-analyzer", 5 | "@semantic-release/release-notes-generator", 6 | "@semantic-release/changelog", 7 | "@semantic-release/npm", 8 | "@semantic-release/git", 9 | ["@semantic-release/github", { 10 | "releasedLabels": ["on:main", "released"] 11 | }] 12 | ], 13 | "branches": ["main"] 14 | } 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [5.0.0](https://github.com/homer0/jsdoc-ts-utils/compare/4.0.0...5.0.0) (2023-10-02) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * drop Node 16 support ([b66b825](https://github.com/homer0/jsdoc-ts-utils/commit/b66b8257b733aa3990d00a6f90703cf700ae8049)) 7 | 8 | 9 | ### BREAKING CHANGES 10 | 11 | * Node 16 is not longer supported. Node 18.17 is the minimum required version now. 12 | 13 | # [4.0.0](https://github.com/homer0/jsdoc-ts-utils/compare/3.1.0...4.0.0) (2023-03-18) 14 | 15 | 16 | ### Bug Fixes 17 | 18 | * make Node 16 the min required version ([b07e627](https://github.com/homer0/jsdoc-ts-utils/commit/b07e627d9ff41a2230b3211c1dcf568ed98b2c04)) 19 | * update dependencies ([5525330](https://github.com/homer0/jsdoc-ts-utils/commit/5525330d0ec3eda6f8c2cf20944c1832bdf5f269)) 20 | 21 | 22 | ### BREAKING CHANGES 23 | 24 | * This package now requires Node 16 or above 25 | 26 | # [3.1.0](https://github.com/homer0/jsdoc-ts-utils/compare/3.0.0...3.1.0) (2022-10-29) 27 | 28 | 29 | ### Bug Fixes 30 | 31 | * avoid processing blocks with ignore tag ([a35a8f2](https://github.com/homer0/jsdoc-ts-utils/commit/a35a8f23ecec6c94ce419e3b514f75960240815d)) 32 | * do not require newline for parsed comments ([4444d82](https://github.com/homer0/jsdoc-ts-utils/commit/4444d82dae7ba37500725d81ce8d3d1133e74638)) 33 | * update dependencies ([5253ad6](https://github.com/homer0/jsdoc-ts-utils/commit/5253ad6f34388aa17ec0d0a58edcc75f135ad657)) 34 | 35 | 36 | ### Features 37 | 38 | * add option to remove single tags ([ec0cdae](https://github.com/homer0/jsdoc-ts-utils/commit/ec0cdae9016c6e3bdb48428a11c43118ea9d98de)) 39 | * add option to remove tagged blocks ([75d7558](https://github.com/homer0/jsdoc-ts-utils/commit/75d7558648d56f6fb104db31d3c165472e0cdc7f)) 40 | 41 | # [3.0.0](https://github.com/homer0/jsdoc-ts-utils/compare/2.0.1...3.0.0) (2022-05-20) 42 | 43 | 44 | ### Bug Fixes 45 | 46 | * drop support for Node 12 ([2cf5881](https://github.com/homer0/jsdoc-ts-utils/commit/2cf588129b212c1b86de1288ffee419825dfac46)) 47 | 48 | 49 | ### BREAKING CHANGES 50 | 51 | * This package no longer supports Node 12. 52 | 53 | ## [2.0.1](https://github.com/homer0/jsdoc-ts-utils/compare/2.0.0...2.0.1) (2021-09-04) 54 | 55 | 56 | ### Bug Fixes 57 | 58 | * update dependencies ([14e0200](https://github.com/homer0/jsdoc-ts-utils/commit/14e020042cd503fbd3111e35dd5b3d4d21ce8643)) 59 | 60 | # [2.0.0](https://github.com/homer0/jsdoc-ts-utils/compare/1.1.2...2.0.0) (2021-04-11) 61 | 62 | 63 | ### Bug Fixes 64 | 65 | * drop support for Node 10 ([daf9f81](https://github.com/homer0/jsdoc-ts-utils/commit/daf9f8182c9da01a642599eeb93a55b40bb0c1ae)) 66 | 67 | 68 | ### BREAKING CHANGES 69 | 70 | * This package no longer supports Node 10. 71 | 72 | ## [1.1.2](https://github.com/homer0/jsdoc-ts-utils/compare/1.1.1...1.1.2) (2020-10-31) 73 | 74 | 75 | ### Bug Fixes 76 | 77 | * **deps:** resolving the yarn problem in the CI ([d674e3b](https://github.com/homer0/jsdoc-ts-utils/commit/d674e3b2dfda80ebdd6b6e691db690b7a54f910d)) 78 | 79 | ## [1.1.1](https://github.com/homer0/jsdoc-ts-utils/compare/1.1.0...1.1.1) (2020-08-24) 80 | 81 | 82 | ### Bug Fixes 83 | 84 | * support multiline typedefs when extending a type ([8ecb515](https://github.com/homer0/jsdoc-ts-utils/commit/8ecb515f91fada146f492e8008d33701d49275e2)) 85 | 86 | # [1.1.0](https://github.com/homer0/jsdoc-ts-utils/compare/1.0.1...1.1.0) (2020-08-24) 87 | 88 | 89 | ### Bug Fixes 90 | 91 | * update dependencies ([2eebbf9](https://github.com/homer0/jsdoc-ts-utils/commit/2eebbf9d5bb90e70de05d338edfeb2ce46c183e0)) 92 | 93 | 94 | ### Features 95 | 96 | * replace `typeof T` with `Class.` ([9a5024e](https://github.com/homer0/jsdoc-ts-utils/commit/9a5024e128b918014c107f6ce0f48f9dacafae7f)) 97 | 98 | ## [1.0.1](https://github.com/homer0/jsdoc-ts-utils/compare/1.0.0...1.0.1) (2020-08-11) 99 | 100 | 101 | ### Bug Fixes 102 | 103 | * update dependencies ([f2d2152](https://github.com/homer0/jsdoc-ts-utils/commit/f2d2152ff4a5b7f5b77c732ceef93e3c31c3fcad)) 104 | 105 | # 1.0.0 (2020-07-14) 106 | 107 | 108 | ### Bug Fixes 109 | 110 | * use the correct name for the JSDoc handler ([c0b94bd](https://github.com/homer0/jsdoc-ts-utils/commit/c0b94bdd8acc524fb5788c9c144012cc3f1b75f7)) 111 | 112 | 113 | ### Code Refactoring 114 | 115 | * rename the main file to index.js ([baefb79](https://github.com/homer0/jsdoc-ts-utils/commit/baefb79a996fc3c5c88e077da5330bb83a974625)) 116 | 117 | 118 | ### Features 119 | 120 | * add the extendTypes feature and its tests ([01294a0](https://github.com/homer0/jsdoc-ts-utils/commit/01294a0ee872d2acf4e7d6e43ccedb60e2abd8bc)) 121 | * add the modules path on memeberof feature and its tests ([80236dd](https://github.com/homer0/jsdoc-ts-utils/commit/80236ddc8db6f1718155a986e0eebfe47f9a18ae)) 122 | * add the modulesTypesShortName feature and its tests ([8d6d015](https://github.com/homer0/jsdoc-ts-utils/commit/8d6d015629b3c4fabac85501f5aef455777afd75)) 123 | * add the parent tag feature ([b28f247](https://github.com/homer0/jsdoc-ts-utils/commit/b28f247a76090b915758fa806e2869d1555acf81)) 124 | * add the plugin basic structure and the typedefImports feature ([cba9aca](https://github.com/homer0/jsdoc-ts-utils/commit/cba9acac56ccd41ecfebb97b8c7958061f27ab51)) 125 | * add the tags replacement feature and its tests ([7aaf9d0](https://github.com/homer0/jsdoc-ts-utils/commit/7aaf9d08c61f31b51c89fd3818e58ff520ed2e56)) 126 | * add the utility types feature and its tests ([734b90d](https://github.com/homer0/jsdoc-ts-utils/commit/734b90de93c2a02350f7aa3a8f970bfaef0ec254)) 127 | * release workflow ([c17a2e8](https://github.com/homer0/jsdoc-ts-utils/commit/c17a2e89d191887ba548a73898bdf11dd68f8b50)) 128 | 129 | 130 | ### BREAKING CHANGES 131 | 132 | * Now, by default, the plugin will look for the @parent tag and replace it with 133 | @memberof 134 | * The file src/plugin.js no longer exists 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSDoc TypeScript utils 2 | 3 | [![GitHub Workflow Status (main)](https://img.shields.io/github/actions/workflow/status/homer0/jsdoc-ts-utils/test.yml?branch=main&style=flat-square)](https://github.com/homer0/jsdoc-ts-utils/actions/workflows/test.yml?query=branch%3Amain) 4 | [![Coveralls GitHub](https://img.shields.io/coveralls/github/homer0/jsdoc-ts-utils.svg?style=flat-square)](https://coveralls.io/github/homer0/jsdoc-ts-utils?branch=main) 5 | ![Libraries.io dependency status for latest release](https://img.shields.io/librariesio/release/npm/jsdoc-ts-utils?style=flat-square) 6 | 7 | A plugin with utilities to make your TypeScript-like docs JSDoc valid 8 | 9 | ## Introduction 10 | 11 | This plugin allows you to take advantage of the [TypeScript](https://www.typescriptlang.org) language server on modern IDEs/editors to generate [intelligent code completion](https://en.wikipedia.org/wiki/Intelligent_code_completion) using your [JSDoc](https://jsdoc.app) comments, and at the same time, be able to generate a documentation site with JSDoc CLI. 12 | 13 | The reason this plugin exists is that JSDoc comments as specified by its _convention_ are not 100% compatible with TypeScript, and they don't cover all the cases; sometimes, writing something that is JSDoc valid can end up killing the code completion, and other times, writing something for the code completion can cause invalid code for the JSDoc CLI. 14 | 15 | The plugin counts with a few (toggleable) features you can use to write code valid for TypeScript that will also be valid for the JSDoc CLI. 16 | 17 | There are also a few features that are designed to make the code compatible with the [ESLint plugin for JSDoc](https://npmjs.com/package/eslint-plugin-jsdoc), highly recommended if you are getting serious with JSDoc. 18 | 19 | ## Configuration 20 | 21 | The first thing you need to do after installing the package, is to add it to the `plugins` array on your JSDoc configuration: 22 | 23 | ```js 24 | { 25 | // ... 26 | "plugins": [ 27 | "jsdoc-ts-utils", 28 | // ... 29 | ] 30 | } 31 | ``` 32 | 33 | > It's important do add it first as it makes modifications to the code in order to make it valid. If a plugin that requires the code to be valid gets executed first, you may end up with unexpected errors. 34 | 35 | Since JSDoc doesn't allow to add configuration options on the `plugins` list, if you need to change the settings, you'll need to create a `tsUtils` object: 36 | 37 | ```js 38 | { 39 | // ... 40 | "plugins": [ 41 | "jsdoc-ts-utils", 42 | // ... 43 | ], 44 | "tsUtils": { 45 | "typedefImports": true, 46 | "typeOfTypes": true, 47 | "extendTypes": true, 48 | "modulesOnMemberOf": true, 49 | "modulesTypesShortName": true, 50 | "parentTag": true, 51 | "removeTaggedBlocks": true, 52 | "removeTags": true, 53 | "typeScriptUtilityTypes": true, 54 | "tagsReplacement": {} 55 | } 56 | } 57 | ``` 58 | 59 | | Option | Default | Description | 60 | | ------ | ------- | ----------- | 61 | | `typedefImports` | `true` | Whether or not to enable the feature that removes `typedef` statements that use `import`. | 62 | | `typeOfTypes` | `true` | Whether or not to enable the feature that replaces `{typeof T}` with `{Class.}`. | 63 | | `extendTypes` | `true` | Whether or not to enable the feature that allows intersections to be reformatted. | 64 | | `modulesOnMemberOf` | `true` | Whether or not to enable the feature that fixes modules' paths on `memeberof` so they can use dot notation. | 65 | | `modulesTypesShortName` | `true` | Whether or not to register modules types without the module path too. | 66 | | `parentTag` | `true` | Whether or not to transform all `parent` tags into `memberof`. | 67 | | `removeTaggedBlocks` | `true` | Whether or not to remove all blocks that have a `@jsdoc-remove` tag. | 68 | | `removeTags` | `true` | Whether or not to remove all tags that follow a `@jsdoc-remove-next-tag` tag. | 69 | | `typeScriptUtilityTypes` | `true` | Whether or not to add the external utility types from TypeScript. | 70 | | `tagsReplacement` | `null` | A dictionary of tags to replace, they keys are the tags being used and the values the tag that should be used. | 71 | 72 | ## Features 73 | 74 | ### Import type defintions 75 | 76 | ```js 77 | /** 78 | * @typedef {import('./daughters').Rosario} Rosario 79 | */ 80 | ``` 81 | 82 | This syntax can be used to import a type from another file without having to import a variable that you won't be using. It not only allows you to import the type of an existing object, but also a type defined with `@typedef`. 83 | 84 | The feature will detect the block and replace it with empty lines (so it doesn't mess up the lines of other definitions on the generated site). 85 | 86 | In case you want to import the type but show it as an external on the site, because it's the type of an installed library and you want to reference it or something, you could use the following syntax: 87 | 88 | ```js 89 | /** 90 | * @typedef {import('family/daughters').Pilar} Pilar 91 | * @external Pilar 92 | * @see https://npmjs.com/package/family 93 | */ 94 | ``` 95 | 96 | The feature will only replace the line for the `@typedef` and leave the rest. 97 | 98 | This is enabled by default but you can disable it with the `typedefImports` option. 99 | 100 | ### Use `typeof` as a type 101 | 102 | ```js 103 | /** 104 | * @typedef {typeof Rosario} ClassRosario 105 | */ 106 | ``` 107 | 108 | One of the most "complicated" things you'll find when typing with JSDoc is how to type class constructors. Let's say a function receives a parameter that is not an instance of the class but its constructor, the `@param` can't be the type of the class: you won't get the autocomplete if you call `new` on it. 109 | 110 | Using the previous feature you can define a `@typedef` with an `import` to the file and the brackets syntax (`import('...')['MyClass']`) to get the constructor reference... but what if you are on the same file as the class? that's when you use `{typeof MyClass}`. 111 | 112 | The `typeof Class` inside a type is not valid JSDoc, so this feature will transform it in order to use the convention `Class.`: 113 | 114 | ```js 115 | /** 116 | * @typedef {Class.} ClassRosario 117 | */ 118 | ``` 119 | 120 | This is enabled by default but you can disable it with the `typeOfTypes` option. 121 | 122 | ### Extending existing types 123 | 124 | ```js 125 | /** 126 | * @typedef {Object} Entity 127 | * @property {string} shape ... 128 | * @property {string} name ... 129 | */ 130 | 131 | /** 132 | * @typedef {Entity & PersonProperties} Person 133 | */ 134 | 135 | /** 136 | * @typedef {Object} PersonProperties 137 | * @property {number} age ... 138 | * @property {number} height ... 139 | * @augments Person 140 | */ 141 | ``` 142 | 143 | You can extend an existing type by defining a new one with the new attributes/properties and another one that intersect it with the original. 144 | 145 | The feature will find the one with the intersection, look if there's a type that `aguments`/`extends` it, remove the intersection, move the attributes/properties to its own definition and remove the extra definition: 146 | 147 | ```js 148 | /** 149 | * @typedef {Object} Entity 150 | * @property {string} shape ... 151 | * @property {string} name ... 152 | */ 153 | 154 | /** 155 | * @typedef {Entity} Person 156 | * @property {number} age ... 157 | * @property {number} height ... 158 | */ 159 | ``` 160 | 161 | If the feature can't find a type the `aguments`/`extends` the intersection, it will simply transform it into a union. 162 | 163 | > **Note:** You should always define the attributes/properties type after the intersection type, so when the feature removes it, it won't mess up the lines of other definitions on the generated site. 164 | 165 | This is enabled by default but you can disable it with the `extendTypes` option. 166 | 167 | ### Modules' paths on @memberof 168 | 169 | ```js 170 | /** 171 | * @typedef {Object} Entity 172 | * @property {string} shape ... 173 | * @property {string} name ... 174 | * @memberof module.services.utils 175 | */ 176 | ``` 177 | 178 | This is meant to solve issues with the ESLint plugin: If the plugin is configured for TypeScript, you can't use the `module:` prefix on `@memberof`, as the parser doesn't support it. 179 | 180 | Adding modules to definitions is something really useful to group parts of your project on the generated site, so this feature allows you to use dot notation: `module.[path]` instead of `module:[path]` and it will automatically transform it before the JSDoc CLI reads it. 181 | 182 | This is enabled by default but you can disable it with the `modulesOnMemberOf` option. 183 | 184 | ### Modules' types short names 185 | 186 | ```js 187 | /** 188 | * @typedef {Object} Entity 189 | * @property {string} shape ... 190 | * @property {string} name ... 191 | * @memberof module.services.utils 192 | */ 193 | 194 | /** 195 | * @param {Entity} entity 196 | * ... 197 | */ 198 | ``` 199 | 200 | When you add `@memberof` to a type definition, you cannot longer reference the type by its name alone, you have to use the `module:[path].[type]` format for the JSDoc CLI to properly link it... not great. 201 | 202 | This features intercepts the creation of the links for types on the generated site and if the type has the `module:` prefix, it will also register it without the prefix as an alias. 203 | 204 | Something similar happens with externals; when you want to reference an external, you need to use `externa:[type]`... yes, the feature takes care of that too. 205 | 206 | This is enabled by default but you can disable it with the `modulesTypesShortName` option. 207 | 208 | ### @parent tag 209 | 210 | ```js 211 | /** 212 | * @typedef {Object} Entity 213 | * @property {string} shape ... 214 | * @property {string} name ... 215 | * @parent module:services/utils 216 | */ 217 | ``` 218 | 219 | If you use special characters on your modules names, like `/`, then `modulesOnMemberOf` won't be enough to help you: the parser the ESLint plugin uses for TypeScript only allows dot notation. 220 | 221 | This feature is simply an alias for `@memberof`: you put whatever you want in the `@parent` tag, and before generating the site, it will be converted to `@memberof`. 222 | 223 | > If you are taking advantage of this feature and using the ESLint plugin, you should add `parent` to the `definedTags` option of the `jsdoc/check-tag-names` rule. 224 | 225 | This is enabled by default but you can disable it with the `parentTag` option. 226 | 227 | ### Remove blocks 228 | 229 | ```js 230 | /** 231 | * @typedef {JQuery.AjaxSettings['success']} JQueryOnSuccess 232 | * @jsdoc-remove 233 | */ 234 | ``` 235 | 236 | Sometimes you have types that could make the site generation fail, and you can't fix them with any of the other features this plugin provides. For those cases, you can use the `@jsdoc-remove` tag to completely remove the block before it even gets processed by the JSDoc CLI. 237 | 238 | This is enabled by default but you can disable it with the `removeTaggedBlocks` option. 239 | 240 | ### Remove tags 241 | 242 | ```js 243 | /** 244 | * @jsdoc-remove-next-tag 245 | * @typedef {JQuery.AjaxSettings['success']} JQueryOnSuccess 246 | * @external JQueryOnSuccess 247 | * @see {@link http://api.jquery.com/jQuery.ajax/#success} 248 | */ 249 | ``` 250 | 251 | While the `jsdoc-remove` tag may be util to get rid of those blocks that breaks the site generation, sometime you just want to remove one single tag, as separating in multiple blocks may make the code hard to read. For those cases, you can use the `@jsdoc-remove-next-tag` tag, and it will only remove the next tag. 252 | 253 | You can even use it multiple times in a single block: 254 | 255 | ```js 256 | /** 257 | * @jsdoc-remove-next-tag 258 | * @typedef {JQuery.AjaxSettings['success']} JQueryOnSuccess 259 | * @external JQueryOnSuccess 260 | * @see {@link http://api.jquery.com/jQuery.ajax/#success} 261 | * @jsdoc-remove-next-tag 262 | * @typedef {JQuery.AjaxSettings['error']} JQueryOnError 263 | * @external JQueryOnError 264 | * @see {@link http://api.jquery.com/jQuery.ajax/#error} 265 | */ 266 | ``` 267 | 268 | This is enabled by default but you can disable it with the `removeTags` option. 269 | 270 | ### TypeScript utility types 271 | 272 | ```js 273 | /** 274 | * @typedef {Object} Entity 275 | * @property {string} shape ... 276 | * @property {string} name ... 277 | */ 278 | 279 | /** 280 | * @param {Partial} entity 281 | * ... 282 | */ 283 | ``` 284 | 285 | TypeScript already comes with a set of [utility types](https://www.typescriptlang.org/docs/handbook/utility-types.html) that you can use on your code and that the code completion will understand. 286 | 287 | This feature basically takes those types and define them as `@external`s, so they can have links on the generated site. 288 | 289 | This is enabled by default but you can disable it with the `typeScriptUtilityTypes` option. 290 | 291 | ### Tags replacement 292 | 293 | ```js 294 | /** 295 | * @parametro {string} name 296 | * @parametro {number} age 297 | * @retorno {Entity} 298 | */ 299 | ``` 300 | 301 | This feature doesn't have a specific use case, it was built for the `@parent` tag and I decided to expose as maybe someone would have the need for it. 302 | 303 | The feature allows you to replace tags before generating the site. You define a "replacement dictionary" on the plugin configuration: 304 | 305 | ```js 306 | { 307 | // ... 308 | "plugins": [ 309 | "jsdoc-ts-utils", 310 | // ... 311 | ], 312 | "tsUtils": { 313 | "tagsReplacement": { 314 | "parametro": "param", 315 | "retorno": "returns" 316 | } 317 | } 318 | } 319 | ``` 320 | 321 | And before generating the site, the feature will replace the tags from the keys with the ones from the values. 322 | 323 | No modification to the `tagsReplacement` option will affect the `@parent` tag feature, they use different instances. 324 | 325 | ## Development 326 | 327 | ### Scripts 328 | 329 | | Task | Description | 330 | |------------|--------------------------------------| 331 | | `docs` | Generates the project documentation. | 332 | | `lint` | Lints the staged files. | 333 | | `lint:all` | Lints the entire project code. | 334 | | `test` | Runs the project unit tests. | 335 | | `todo` | Lists all the pending to-do's. | 336 | 337 | ### Repository hooks 338 | 339 | I use [`husky`](https://npmjs.com/package/husky) to automatically install the repository hooks so the code will be tested and linted before any commit, and the dependencies updated after every merge. 340 | 341 | #### Commits convention 342 | 343 | I use [conventional commits](https://www.conventionalcommits.org) with [`commitlint`](https://commitlint.js.org) in order to support semantic releases. The one that sets it up is actually husky, that installs a script that runs `commitlint` on the `git commit` command. 344 | 345 | The configuration is on the `commitlint` property of the `package.json`. 346 | 347 | ### Releases 348 | 349 | I use [`semantic-release`](https://npmjs.com/package/semantic-release) and a GitHub action to automatically release on NPM everything that gets merged to main. 350 | 351 | The configuration for `semantic-release` is on `./releaserc` and the workflow for the release is on `./.github/workflow/release.yml`. 352 | 353 | ### Testing 354 | 355 | I use [Jest](https://facebook.github.io/jest/) to test the project. 356 | 357 | The configuration file is on `./.jestrc.js`, the tests are on `./tests` and the script that runs it is on `./utils/scripts/test`. 358 | 359 | ### Linting && Formatting 360 | 361 | I use [ESlint](https://eslint.org) with [my own custom configuration](https://npmjs.com/package/@homer0/eslint-plugin) to validate all the JS code. The configuration file for the project code is on `./.eslintrc` and the one for the tests is on `./tests/.eslintrc`. There's also an `./.eslintignore` to exclude some files on the process. The script that runs it is on `./utils/scripts/lint-all`. 362 | 363 | For formatting I use [Prettier](https://prettier.io) with [my custom configuration](https://npmjs.com/package/@homer0/prettier-config). The configuration file for the project code is on `./.prettierrc`. 364 | 365 | ### Documentation 366 | 367 | I use [JSDoc](https://jsdoc.app) to generate an HTML documentation site for the project. 368 | 369 | The configuration file is on `./.jsdoc.js` and the script that runs it is on `./utils/scripts/docs`. 370 | 371 | ### To-Dos 372 | 373 | I use `@todo` comments to write all the pending improvements and fixes, and [Leasot](https://npmjs.com/package/leasot) to generate a report. 374 | 375 | The script that runs it is on `./utils/scripts/todo`. 376 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsdoc-ts-utils", 3 | "description": "A plugin with utilities to make your TypeScript-like docs JSDoc valid", 4 | "homepage": "https://github.com/homer0/jsdoc-ts-utils", 5 | "version": "5.0.0", 6 | "repository": "homer0/jsdoc-ts-utils", 7 | "author": "Leonardo Apiwan (@homer0) ", 8 | "license": "MIT", 9 | "keywords": [ 10 | "jsdoc", 11 | "typescript", 12 | "ts", 13 | "documentation", 14 | "typedef" 15 | ], 16 | "dependencies": { 17 | "jsdoc": "^4.0.2" 18 | }, 19 | "devDependencies": { 20 | "@commitlint/cli": "^17.7.2", 21 | "@commitlint/config-conventional": "^17.7.0", 22 | "@homer0/eslint-plugin": "^12.0.0", 23 | "@homer0/prettier-config": "^1.1.3", 24 | "@homer0/prettier-plugin-jsdoc": "^8.0.0", 25 | "@semantic-release/changelog": "^6.0.3", 26 | "@semantic-release/git": "^10.0.1", 27 | "docdash": "homer0/docdash#semver:^2.1.2", 28 | "eslint": "^8.50.0", 29 | "husky": "^8.0.3", 30 | "is-ci": "^3.0.1", 31 | "jest": "^29.7.0", 32 | "leasot": "^13.3.0", 33 | "lint-staged": "^14.0.1", 34 | "prettier": "^3.0.3", 35 | "semantic-release": "^22.0.5" 36 | }, 37 | "peerDependencies": { 38 | "jsdoc": "*" 39 | }, 40 | "engine-strict": true, 41 | "engines": { 42 | "node": ">=18.17 <21" 43 | }, 44 | "lint-staged": { 45 | "*.js": [ 46 | "eslint", 47 | "prettier --write" 48 | ] 49 | }, 50 | "commitlint": { 51 | "extends": [ 52 | "@commitlint/config-conventional" 53 | ] 54 | }, 55 | "main": "src/index.js", 56 | "scripts": { 57 | "docs": "./utils/scripts/docs", 58 | "lint": "./utils/scripts/lint", 59 | "lint:all": "./utils/scripts/lint-all", 60 | "test": "./utils/scripts/test", 61 | "todo": "./utils/scripts/todo", 62 | "prepare": "./utils/scripts/prepare" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** 3 | * @typedef {Object} EventNames 4 | * @property {string} parseBegin The event triggered before the files are parsed. 5 | * @property {string} newComment The event triggered when the plugin finds a new JSDoc 6 | * comment. 7 | * @property {string} commentsReady The event triggered when all the comments were 8 | * analyzed and the features should start making 9 | * modifications to the code. 10 | */ 11 | 12 | /** 13 | * A dictionary with the different events the plugin uses. 14 | * 15 | * @type {EventNames} 16 | * @ignore 17 | */ 18 | const EVENT_NAMES = { 19 | parseBegin: 'jsdoc:parse-begin', 20 | newComment: 'ts-utils:new-comment', 21 | commentsReady: 'ts-utils:comments-ready', 22 | }; 23 | 24 | module.exports.EVENT_NAMES = EVENT_NAMES; 25 | -------------------------------------------------------------------------------- /src/features/extendTypes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Object} ExtendTypesCommentWithProperties 3 | * @property {string} name The name of the type. 4 | * @property {string} augments The name of the type it extends. 5 | * @property {string} comment The raw comment. 6 | * @property {number} linesCount How many lines the comment has. 7 | * @property {string[]} lines The list of lines from the comment that can be used on 8 | * the type that it's being extended. 9 | * @property {number} usedLines This counter gets modified during parsing and it 10 | * represents how many of the `lines` were used: if the 11 | * type with the intersection already has one line, it's 12 | * not added, but the class needs to know how many lines 13 | * it needs to use to replace the comment when removed. 14 | * @ignore 15 | */ 16 | 17 | /** 18 | * This class allows the use of intersection types in oder to define types extensions by 19 | * transforming the code in two ways: 20 | * 1. If one of the types of the intersection uses `augments`/`extends` for the intersection type, 21 | * all its "lines" are moved to the intersection type and it gets removed. 22 | * 2. If the first scenario is not possible, it trasforms `&` into `|`. Yes, it becomes a union 23 | * type, but it's as closer as we can get with pure JSDoc. 24 | */ 25 | class ExtendTypes { 26 | /** 27 | * @param {EventEmitter} events To hook to the necessary events to parse the code. 28 | * @param {EventNames} EVENT_NAMES To get the name of the events the class needs to 29 | * listen for. 30 | */ 31 | constructor(events, EVENT_NAMES) { 32 | /** 33 | * The list of comments that use intersections on a `typedef` statements. 34 | * 35 | * @type {string[]} 36 | * @access protected 37 | * @ignore 38 | */ 39 | this._commentsWithIntersections = []; 40 | /** 41 | * A list with the information of comments that extend/agument types with 42 | * intersections. 43 | * 44 | * @type {ExtendTypesCommentWithProperties[]} 45 | * @access protected 46 | * @ignore 47 | */ 48 | this._commentsWithProperties = []; 49 | // Setup the listeners. 50 | events.on(EVENT_NAMES.newComment, this._readComment.bind(this)); 51 | events.on(EVENT_NAMES.commentsReady, this._replaceComments.bind(this)); 52 | } 53 | /** 54 | * Given a list of types from an instersection definition, this methods tries to find a 55 | * comment that extends one of those types. 56 | * If the method finds only one comment for a type, it returns it, otherwise, it returns 57 | * `null`: 58 | * the class only supports extending one type. 59 | * 60 | * @param {string} name The name of the type that uses intersection. 61 | * @param {string[]} types The list of types in the intersection. 62 | * @returns {?ExtendTypesCommentWithProperties} 63 | * @access protected 64 | * @ignore 65 | */ 66 | _getCommentWithProperties(name, types) { 67 | const comments = types 68 | .map((type) => 69 | this._commentsWithProperties.find( 70 | (comment) => comment.name === type && comment.augments === name, 71 | ), 72 | ) 73 | .filter((comment) => comment); 74 | 75 | return comments.length === 1 ? comments[0] : null; 76 | } 77 | /** 78 | * Parses a JSDock comment block for a type definition that extends a type with 79 | * intersection. 80 | * 81 | * @param {string} comment The raw comment to parse. 82 | * @returns {ExtendTypesCommentWithProperties} 83 | * @access protected 84 | * @ignore 85 | */ 86 | _getCommentWithPropertiesInfo(comment) { 87 | const [, name] = /@typedef\s+\{[^\}]+\}[\s\*]*(.*?)\s/i.exec(comment); 88 | const [, augments] = /@(?:augments|extends)\s+(.*?)\s/i.exec(comment); 89 | const allLines = comment.split('\n'); 90 | const linesCount = allLines.length; 91 | const lines = allLines.filter( 92 | (line) => 93 | line.match(/\w/) && !line.match(/^\s*\*\s*@(?:typedef|augments|extends)\s+/i), 94 | ); 95 | return { 96 | name: name.trim(), 97 | augments: augments.trim(), 98 | comment, 99 | linesCount, 100 | usedLines: 0, 101 | lines, 102 | }; 103 | } 104 | /** 105 | * This is called every time a new JSDoc comment block is found on a file. It validates 106 | * if the block uses intersection or if it's a `typedef` that extends another type in 107 | * order to save it to be parsed later. 108 | * 109 | * @param {string} comment The comment to analyze. 110 | * @access protected 111 | * @ignore 112 | */ 113 | _readComment(comment) { 114 | if (comment.match(/\*\s*@typedef\s+\{\s*\w+\s*&\s*\w+/i)) { 115 | this._commentsWithIntersections.push(comment); 116 | } else if ( 117 | comment.match(/\*\s*@typedef\s+\{/i) && 118 | comment.match(/\*\s*@(?:augments|extends)\s+\w+/i) 119 | ) { 120 | this._commentsWithProperties.push(this._getCommentWithPropertiesInfo(comment)); 121 | } 122 | } 123 | /** 124 | * Replaces the JSDoc comment blocks for type definitions that extend types with 125 | * intersections with empty lines. 126 | * 127 | * @param {string} source The code of the file where the comments should be removed. 128 | * @returns {string} 129 | * @access protected 130 | * @ignore 131 | */ 132 | _removeCommentsWithProperties(source) { 133 | return this._commentsWithProperties.reduce( 134 | (acc, comment) => 135 | acc.replace( 136 | comment.comment, 137 | new Array(comment.linesCount - comment.usedLines).fill('').join('\n'), 138 | ), 139 | source, 140 | ); 141 | } 142 | /** 143 | * This is called after all the JSDoc comments block for a file were found and the 144 | * plugin is ready to make changes. 145 | * The method first _"fixes"_ the `typedef` statements with intersections and then 146 | * removes the comments for types that extend another types. 147 | * 148 | * @param {string} source The code of the file being parsed. 149 | * @returns {string} 150 | * @access protected 151 | * @ignore 152 | */ 153 | _replaceComments(source) { 154 | let result = this._replaceDefinitions(source); 155 | result = this._removeCommentsWithProperties(result); 156 | this._commentsWithIntersections = []; 157 | this._commentsWithProperties = []; 158 | return result; 159 | } 160 | /** 161 | * Parses the comments with intersections, validate if there's a type extending them 162 | * from where they can take "lines", or if they should be transformed into unions. 163 | * 164 | * @param {string} source The code of the file where the comments should be replaced. 165 | * @returns {string} 166 | * @access protected 167 | * @ignore 168 | */ 169 | _replaceDefinitions(source) { 170 | return this._commentsWithIntersections.reduce((acc, comment) => { 171 | // Extract the `typedef` types and name. 172 | const [typedefLine, rawTypes, name] = /@typedef\s*\{([^\}]+)\}[\s\*]*(.*?)\s/i.exec( 173 | comment, 174 | ); 175 | // Transform the types into an array. 176 | const types = rawTypes.split('&').map((type) => type.trim()); 177 | 178 | let replacement; 179 | // Try to find a type that extends this one. 180 | const commentWithProps = this._getCommentWithProperties(name, types); 181 | if (commentWithProps) { 182 | // If there was a type extending it... 183 | // Find the "real type" that it's being extended. 184 | const [baseType] = types.filter((type) => type !== commentWithProps.name); 185 | // Remove the intersection and leave just the "real type" 186 | const newTypedefLine = typedefLine.replace(rawTypes, baseType); 187 | // Replace the `typedef` with the new one, with the "real type". 188 | const newComment = comment.replace(typedefLine, newTypedefLine); 189 | // Transform the comment into an array of lines. 190 | const lines = newComment.split('\n'); 191 | // Remove the line with `*/` so new lines can be added. 192 | const closingLine = lines.pop(); 193 | /** 194 | * For each line of the type that extends the definition, check if it doesn't 195 | * already exists on the comment (this can happen with `access` or `memberof` 196 | * statements), 197 | * and if it doesn't, it not only adds it but it also increments the counter that 198 | * tracks how many lines of the comment are being used. 199 | * 200 | * The lines that are moved are counted so the class can later replace the comment 201 | * with enough empty lines so it won't mess up the line number of the rest of the 202 | * types. 203 | * For example: If we had a type with intersection with a single line, a type that 204 | * extends it with 3 lines lines with `property` and one with `access` 205 | * (plus the `typedef` and the `extends`/`augments`) and the intersection type 206 | * already has `access`, the class would replace the comment with `5` empty lines. 207 | * The comment had a total of 8 lines, 3 `property`, `typedef`, 208 | * `extends`/`augments`, `access` and the opening and closing lines; but the 209 | * properties were moved to another type. 210 | * 211 | * @ignore 212 | */ 213 | const info = commentWithProps.lines.reduce( 214 | (infoAcc, line) => { 215 | let nextInfoAcc; 216 | if (infoAcc.lines.includes(line)) { 217 | nextInfoAcc = infoAcc; 218 | } else { 219 | nextInfoAcc = { 220 | lines: [...infoAcc.lines, line], 221 | count: infoAcc.count + 1, 222 | }; 223 | } 224 | 225 | return nextInfoAcc; 226 | }, 227 | { 228 | lines, 229 | count: commentWithProps.usedLines, 230 | }, 231 | ); 232 | // Add the closing line and put the comment back together. 233 | info.lines.push(closingLine); 234 | replacement = info.lines.join('\n'); 235 | // Update the counter with the used lines. 236 | commentWithProps.usedLines = info.count; 237 | } else { 238 | // No comment was found, so transform the intersection into a union. 239 | const newTypedefLine = typedefLine.replace(rawTypes, types.join('|')); 240 | replacement = comment.replace(typedefLine, newTypedefLine); 241 | } 242 | 243 | return acc.replace(comment, replacement); 244 | }, source); 245 | } 246 | } 247 | 248 | module.exports.ExtendTypes = ExtendTypes; 249 | -------------------------------------------------------------------------------- /src/features/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | module.exports.ExtendTypes = require('./extendTypes').ExtendTypes; 4 | module.exports.ModulesOnMemberOf = require('./modulesOnMemberOf').ModulesOnMemberOf; 5 | module.exports.ModulesTypesShortName = 6 | require('./modulesTypesShortName').ModulesTypesShortName; 7 | module.exports.RemoveTaggedBlocks = require('./removeTaggedBlocks').RemoveTaggedBlocks; 8 | module.exports.RemoveTags = require('./removeTags').RemoveTags; 9 | module.exports.TagsReplacement = require('./tagsReplacement').TagsReplacement; 10 | module.exports.TSUtilitiesTypes = require('./tsUtilityTypes').TSUtilitiesTypes; 11 | module.exports.TypedefImports = require('./typedefImports').TypedefImports; 12 | module.exports.TypeOfTypes = require('./typeOfTypes').TypeOfTypes; 13 | -------------------------------------------------------------------------------- /src/features/modulesOnMemberOf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** 3 | * This class replaces modules' paths on `memberof` statements that use dot notation in order to 4 | * make them JSDoc valid: `module.something` becomes `module:something`. 5 | */ 6 | class ModulesOnMemberOf { 7 | /** 8 | * @param {EventEmitter} events To hook to the necessary events to parse the code. 9 | * @param {EventNames} EVENT_NAMES To get the name of the events the class needs to 10 | * listen for. 11 | */ 12 | constructor(events, EVENT_NAMES) { 13 | // Setup the listener. 14 | events.on(EVENT_NAMES.commentsReady, this._fixModulesPaths.bind(this)); 15 | } 16 | /** 17 | * This is called by the plugin in order to fix the modules' paths. 18 | * 19 | * @param {string} source The code of the file being parsed. 20 | * @returns {string} 21 | * @access protected 22 | * @ignore 23 | */ 24 | _fixModulesPaths(source) { 25 | return source.replace( 26 | /^(\s+\*\s*@memberof!?\s*)(module\.)(?!exports[$\s])/gim, 27 | '$1module:', 28 | ); 29 | } 30 | } 31 | 32 | module.exports.ModulesOnMemberOf = ModulesOnMemberOf; 33 | -------------------------------------------------------------------------------- /src/features/modulesTypesShortName.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** 3 | * This class monkey-patches the function JSDoc uses to register links in order to create "aliases" 4 | * for external types and modules' types so they can be used without the `external:` and 5 | * `module:[name].` prefixes. 6 | * For example: `module:shared/apiClient.APIClient` would also register `APIClient`; and 7 | * `external:Express` would also register `Express`. 8 | */ 9 | class ModulesTypesShortName { 10 | /** 11 | * @param {EventEmitter} events To hook to the event triggered when 12 | * parsing begins. 13 | * @param {JSDocTemplateHelper} jsdocTemplateHelper To monkey-patch the `registerLink` 14 | * function. 15 | * @param {EventNames} EVENT_NAMES To get the name of the event the 16 | * class needs to listen for. 17 | */ 18 | constructor(events, jsdocTemplateHelper, EVENT_NAMES) { 19 | /** 20 | * A local reference for the JSDoc template helper, where the `registerLink` function 21 | * will be monkey-patched. 22 | * 23 | * @type {JSDocTemplateHelper} 24 | * @access protected 25 | * @ignore 26 | */ 27 | this._jsdocTemplateHelper = jsdocTemplateHelper; 28 | // Setup the listener. 29 | events.on(EVENT_NAMES.parseBegin, this._monkyPatchRegisterLink.bind(this)); 30 | } 31 | /** 32 | * This is called by the plugin before starting to parse the files; the method takes 33 | * care of validating if the `registerLink` function of the template helper was already 34 | * patched and patch it if it wasn't. 35 | * 36 | * @access protected 37 | * @ignore 38 | */ 39 | _monkyPatchRegisterLink() { 40 | // Check if the function needs patching. 41 | if (!('monkey' in this._jsdocTemplateHelper.registerLink)) { 42 | // Get a reference for the original. 43 | const original = this._jsdocTemplateHelper.registerLink; 44 | /** 45 | * The patch. 46 | * 47 | * @type {JSDocTemplateHelperRegisterLink} 48 | * @ignore 49 | */ 50 | const patch = (longname, fileUrl) => { 51 | // Call the original function. 52 | const result = original(longname, fileUrl); 53 | // Extract the short name. 54 | const match = 55 | /module:[^\.]+\.((?:[^\.]+\.)?[^\.]+)$/i.exec(longname) || 56 | /external:([^\.]+)$/i.exec(longname); 57 | if (match) { 58 | // If a short name was found, register it. 59 | const [, shortname] = match; 60 | original(shortname, fileUrl); 61 | } 62 | 63 | // Return the original result. 64 | return result; 65 | }; 66 | // Add the flag. 67 | // @ts-ignore 68 | patch.monkey = true; 69 | // Replace the function on the helper. 70 | this._jsdocTemplateHelper.registerLink = patch; 71 | } 72 | } 73 | } 74 | 75 | module.exports.ModulesTypesShortName = ModulesTypesShortName; 76 | -------------------------------------------------------------------------------- /src/features/removeTaggedBlocks.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** 3 | * This class removes blocks that use the `jsdoc-remove` tag. 4 | */ 5 | class RemoveTaggedBlocks { 6 | /** 7 | * @param {EventEmitter} events To hook to the necessary events to parse the code. 8 | * @param {EventNames} EVENT_NAMES To get the name of the events the class needs to 9 | * listen for. 10 | */ 11 | constructor(events, EVENT_NAMES) { 12 | /** 13 | * The list of comments that use `jsdoc-remove`. 14 | * 15 | * @type {string[]} 16 | * @access protected 17 | * @ignore 18 | */ 19 | this._comments = []; 20 | // Setup the listeners. 21 | events.on(EVENT_NAMES.newComment, this._readComment.bind(this)); 22 | events.on(EVENT_NAMES.commentsReady, this._replaceComments.bind(this)); 23 | } 24 | /** 25 | * This is called every time a new JSDoc comment block is found on a file. It validates 26 | * if the block uses `jsdoc-remove` and saves it so it can be parsed later. 27 | * 28 | * @param {string} comment The comment to analyze. 29 | * @access protected 30 | * @ignore 31 | */ 32 | _readComment(comment) { 33 | if (comment.match(/\s*\* @jsdoc-remove\s*\*/)) { 34 | this._comments.push(comment); 35 | } 36 | } 37 | /** 38 | * This is called after all the JSDoc comments block for a file were found and the 39 | * plugin is ready to make changes. 40 | * The method takes all the comments that were found before and replace them with empty 41 | * lines. 42 | * 43 | * @param {string} source The code of the file being parsed. 44 | * @returns {string} 45 | * @access protected 46 | * @ignore 47 | */ 48 | _replaceComments(source) { 49 | const result = this._comments.reduce( 50 | (acc, comment) => 51 | acc.replace( 52 | comment, 53 | comment 54 | .split('\n') 55 | .map(() => '') 56 | .join('\n'), 57 | ), 58 | source, 59 | ); 60 | 61 | this._comments = []; 62 | return result; 63 | } 64 | } 65 | 66 | module.exports.RemoveTaggedBlocks = RemoveTaggedBlocks; 67 | -------------------------------------------------------------------------------- /src/features/removeTags.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** 3 | * This class removes tag lines after a `jsdoc-remove-next-tag` tag is found. 4 | */ 5 | class RemoveTags { 6 | /** 7 | * @param {EventEmitter} events To hook to the necessary events to parse the code. 8 | * @param {EventNames} EVENT_NAMES To get the name of the events the class needs to 9 | * listen for. 10 | */ 11 | constructor(events, EVENT_NAMES) { 12 | /** 13 | * The list of comments that use `jsdoc-remove`. 14 | * 15 | * @type {string[]} 16 | * @access protected 17 | * @ignore 18 | */ 19 | this._comments = []; 20 | /** 21 | * The expression that validates that the `jsdoc-remove-next-tag` tag is being used. 22 | * This expression is used on multiple places, that's why it's declared as a property. 23 | * 24 | * @type {RegExp} 25 | * @access protected 26 | * @ignore 27 | */ 28 | this._removeExpression = /\s*\* @jsdoc-remove-next-tag\s*/; 29 | // Setup the listeners. 30 | events.on(EVENT_NAMES.newComment, this._readComment.bind(this)); 31 | events.on(EVENT_NAMES.commentsReady, this._replaceComments.bind(this)); 32 | } 33 | /** 34 | * This is called every time a new JSDoc comment block is found on a file. It validates 35 | * if the block uses `jsdoc-remove-next-tag` and saves it so it can be parsed later. 36 | * 37 | * @param {string} comment The comment to analyze. 38 | * @access protected 39 | * @ignore 40 | */ 41 | _readComment(comment) { 42 | if (comment.match(this._removeExpression)) { 43 | this._comments.push(comment); 44 | } 45 | } 46 | /** 47 | * This is called after all the JSDoc comments block for a file were found and the 48 | * plugin is ready to make changes. 49 | * The method takes all the comments that were found before and, if the comment includes 50 | * the `jsdoc-remove-next-tag` tag, it will remove the next lines until it finds a new 51 | * tag. 52 | * 53 | * @param {string} source The code of the file being parsed. 54 | * @returns {string} 55 | * @access protected 56 | * @ignore 57 | */ 58 | _replaceComments(source) { 59 | const result = this._comments.reduce((acc, comment) => { 60 | const lines = comment.split('\n'); 61 | let removing = false; 62 | let removedAtLeastOneLine = false; 63 | /** 64 | * @type {string[]} 65 | */ 66 | const newLinesAcc = []; 67 | const newLines = lines.reduce((sacc, line, index) => { 68 | if (index === lines.length - 1) { 69 | sacc.push(line); 70 | return sacc; 71 | } 72 | 73 | if (line.match(this._removeExpression)) { 74 | removing = true; 75 | removedAtLeastOneLine = false; 76 | return sacc; 77 | } 78 | 79 | if (removing && line.match(/^\s*\* @/)) { 80 | if (removedAtLeastOneLine) { 81 | removing = false; 82 | } else { 83 | removedAtLeastOneLine = true; 84 | } 85 | } 86 | 87 | if (!removing) { 88 | sacc.push(line); 89 | } 90 | 91 | return sacc; 92 | }, newLinesAcc); 93 | return acc.replace(comment, newLines.join('\n')); 94 | }, source); 95 | 96 | this._comments = []; 97 | return result; 98 | } 99 | } 100 | 101 | module.exports.RemoveTags = RemoveTags; 102 | -------------------------------------------------------------------------------- /src/features/tagsReplacement.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** 3 | * @typedef {Object.} TagsReplacementDictionary 4 | */ 5 | 6 | /** 7 | * This class allows the replacement of JSDoc tags. For example, it can be used to replace all 8 | * `parent` tags with `memberof`. 9 | */ 10 | class TagsReplacement { 11 | /** 12 | * @param {TagsReplacementDictionary} dictionary The dictionary of tags that need to 13 | * be replaced. 14 | * @param {EventEmitter} events To hook to the even triggered when 15 | * the plugin can do modifications to 16 | * the code. 17 | * @param {EventNames} EVENT_NAMES To get the name of the event the 18 | * class needs to listen for. 19 | */ 20 | constructor(dictionary, events, EVENT_NAMES) { 21 | /** 22 | * The dictionary of tags that are going to be replaced. 23 | * 24 | * @type {TagsReplacementDictionary} 25 | * @access protected 26 | * @ignore 27 | */ 28 | this._dictionary = dictionary; 29 | // Setup the listener. 30 | events.on(EVENT_NAMES.commentsReady, this._replaceTags.bind(this)); 31 | } 32 | /** 33 | * This is called by the plugin in order replace the tags on a file. 34 | * 35 | * @param {string} source The code of the file being parsed. 36 | * @returns {string} 37 | * @access protected 38 | * @ignore 39 | */ 40 | _replaceTags(source) { 41 | return Object.entries(this._dictionary).reduce( 42 | (acc, [original, replacement]) => 43 | acc.replace( 44 | new RegExp(`^(\\s+\\*\\s*@)${original}(\\s+)`, 'gim'), 45 | `$1${replacement}$2`, 46 | ), 47 | source, 48 | ); 49 | } 50 | } 51 | 52 | module.exports.TagsReplacement = TagsReplacement; 53 | -------------------------------------------------------------------------------- /src/features/tsUtilityTypes.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const path = require('path'); 3 | 4 | /** 5 | * This class adds extra type definitions for TypeScript utility types. If the project includes 6 | * a `typedef` file, the definitions will be added at the end of the file; otherwise, they'll be 7 | * added to the first file being parsed. 8 | */ 9 | class TSUtilitiesTypes { 10 | /** 11 | * @param {EventEmitter} events To hook to the necessary events to add the 12 | * definitions. 13 | * @param {EventNames} EVENT_NAMES To get the name of the events the class needs to 14 | * listen for. 15 | */ 16 | constructor(events, EVENT_NAMES) { 17 | /** 18 | * If a `typedef` file is found, this property will have its path. 19 | * 20 | * @type {?string} 21 | * @access protected 22 | * @ignore 23 | */ 24 | this._typedefFile = null; 25 | /** 26 | * A control flag used when parsing the files in order to know if the defintions were 27 | * already added or not. 28 | * 29 | * @type {boolean} 30 | * @access protected 31 | * @ignore 32 | */ 33 | this._added = false; 34 | /** 35 | * The base URL of where the types are documented. 36 | * 37 | * @type {string} 38 | * @access protected 39 | * @ignore 40 | */ 41 | this._typesUrl = 'https://www.typescriptlang.org/docs/handbook/utility-types.html'; 42 | /** 43 | * The dictionary of the utility types. The keys are the names of the types and the 44 | * values their anchor section on the documentation page. 45 | * 46 | * @type {Object.} 47 | * @access protected 48 | * @ignore 49 | */ 50 | this._types = { 51 | Partial: 'partialt', 52 | Readonly: 'readonlyt', 53 | Record: 'recordkt', 54 | Pick: 'picktk', 55 | Omit: 'omittk', 56 | Exclude: 'excludetu', 57 | Extract: 'extracttu', 58 | NonNullable: 'nonnullablet', 59 | Parameters: 'parameterst', 60 | ConstructorParameters: 'constructorparameterst', 61 | ReturnType: 'returntypet', 62 | InstanceType: 'instancetypet', 63 | Required: 'requiredt', 64 | ThisParameterType: 'thisparametertype', 65 | OmitThisParameter: 'omitthisparameter', 66 | ThisType: 'thistypet', 67 | }; 68 | // Setup the listeners. 69 | events.on(EVENT_NAMES.parseBegin, this._findTypedefFile.bind(this)); 70 | events.on(EVENT_NAMES.commentsReady, this._addTypes.bind(this)); 71 | } 72 | /** 73 | * This is called by the plugin in order to add the types. 74 | * 75 | * @param {string} source The code of the file being parsed. 76 | * @param {string} filename The path of the file being parsed. This is used in case a 77 | * `typedef` 78 | * file exists on the project, to validate if the comments 79 | * should be added on that file. 80 | * @returns {string} 81 | * @access protected 82 | * @ignore 83 | */ 84 | _addTypes(source, filename) { 85 | let result; 86 | if (this._added || (this._typedefFile && filename !== this._typedefFile)) { 87 | result = source; 88 | } else { 89 | const comments = this._getComments(); 90 | result = `${source}\n\n${comments}`; 91 | this._added = true; 92 | } 93 | 94 | return result; 95 | } 96 | /** 97 | * This is called by the plugin before the parsing beings, so the class can identify if 98 | * the project includes a `typedef` file. 99 | * 100 | * @param {JSDocParseBeginEventPayload} event The event information, with the list of 101 | * files that going to be parsed. 102 | */ 103 | _findTypedefFile(event) { 104 | const typedef = event.sourcefiles.find((file) => 105 | path.basename(file).match(/^typedef\.[jt]sx?$/), 106 | ); 107 | 108 | this._typedefFile = typedef || null; 109 | this._added = false; 110 | } 111 | /** 112 | * Generates the `typedef` blocks for the TypeScript utility types. 113 | * 114 | * @returns {string} 115 | * @access protected 116 | * @ignore 117 | */ 118 | _getComments() { 119 | return Object.entries(this._types) 120 | .map(([name, anchor]) => [ 121 | '/**', 122 | ` * @external ${name}`, 123 | ` * @see ${this._typesUrl}#${anchor}`, 124 | ' */\n', 125 | ]) 126 | .reduce((acc, lines) => [...acc, ...lines], []) 127 | .join('\n'); 128 | } 129 | } 130 | 131 | module.exports.TSUtilitiesTypes = TSUtilitiesTypes; 132 | -------------------------------------------------------------------------------- /src/features/typeOfTypes.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** 3 | * This class takes care of fixing JSDoc comments that use `typeof T` as a type and replace it with 4 | * `Class.`. 5 | */ 6 | class TypeOfTypes { 7 | /** 8 | * @param {EventEmitter} events To hook to the necessary events to parse the code. 9 | * @param {EventNames} EVENT_NAMES To get the name of the events the class needs to 10 | * listen for. 11 | */ 12 | constructor(events, EVENT_NAMES) { 13 | /** 14 | * The list of comments that use `typeof` on a type. 15 | * 16 | * @type {string[]} 17 | * @access protected 18 | * @ignore 19 | */ 20 | this._comments = []; 21 | // Setup the listeners. 22 | events.on(EVENT_NAMES.newComment, this._readComment.bind(this)); 23 | events.on(EVENT_NAMES.commentsReady, this._replaceComments.bind(this)); 24 | } 25 | /** 26 | * This is called every time a new JSDoc comment block is found on a file. It validates 27 | * if the block uses `typeof` as a type and saves it so it can be parsed later. 28 | * 29 | * @param {string} comment The comment to analyze. 30 | * @access protected 31 | * @ignore 32 | */ 33 | _readComment(comment) { 34 | if (comment.match(/\{\s*typeof\s+\w+\s*}/i)) { 35 | this._comments.push(comment); 36 | } 37 | } 38 | /** 39 | * This is called after all the JSDoc comments block for a file were found and the 40 | * plugin is ready to make changes. 41 | * The method takes all the comments that were found before and, if the comment includes 42 | * a `typeof` on a type, it replaces it with `Class.`. 43 | * 44 | * @param {string} source The code of the file being parsed. 45 | * @returns {string} 46 | * @access protected 47 | * @ignore 48 | */ 49 | _replaceComments(source) { 50 | const result = this._comments.reduce( 51 | (acc, comment) => 52 | acc.replace( 53 | comment, 54 | comment.replace(/\{\s*typeof\s+(\w+)\s*}/gi, '{Class.<$1>}'), 55 | ), 56 | source, 57 | ); 58 | 59 | this._comments = []; 60 | return result; 61 | } 62 | } 63 | 64 | module.exports.TypeOfTypes = TypeOfTypes; 65 | -------------------------------------------------------------------------------- /src/features/typedefImports.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** 3 | * This class takes care of removing blocks of JSDoc comments that use `typedef` statements with 4 | * `import(...)`. 5 | */ 6 | class TypedefImports { 7 | /** 8 | * @param {EventEmitter} events To hook to the necessary events to parse the code. 9 | * @param {EventNames} EVENT_NAMES To get the name of the events the class needs to 10 | * listen for. 11 | */ 12 | constructor(events, EVENT_NAMES) { 13 | /** 14 | * The list of comments that use `typedef` with `import(...)`. 15 | * 16 | * @type {string[]} 17 | * @access protected 18 | * @ignore 19 | */ 20 | this._comments = []; 21 | /** 22 | * The expression that validates that `import(...)` is being used inside a `typedef` 23 | * statement. 24 | * This expression is used on multiple places, that's why it's declared as a property. 25 | * 26 | * @type {RegExp} 27 | * @access protected 28 | * @ignore 29 | */ 30 | this._importExpression = /\{\s*import\s*\(/i; 31 | // Setup the listeners. 32 | events.on(EVENT_NAMES.newComment, this._readComment.bind(this)); 33 | events.on(EVENT_NAMES.commentsReady, this._replaceComments.bind(this)); 34 | } 35 | /** 36 | * This is called every time a new JSDoc comment block is found on a file. It validates 37 | * if the block uses `import(...)` and saves it so it can be parsed later. 38 | * 39 | * @param {string} comment The comment to analyze. 40 | * @access protected 41 | * @ignore 42 | */ 43 | _readComment(comment) { 44 | if (comment.match(this._importExpression)) { 45 | this._comments.push(comment); 46 | } 47 | } 48 | /** 49 | * This is called after all the JSDoc comments block for a file were found and the 50 | * plugin is ready to make changes. 51 | * The method takes all the comments that were found before and, if the comment includes 52 | * an `external` statement, it just replaces the `typedef` line with an empty one; but 53 | * if it doesn't, it gets replaced with empty lines. 54 | * 55 | * @param {string} source The code of the file being parsed. 56 | * @returns {string} 57 | * @access protected 58 | * @ignore 59 | */ 60 | _replaceComments(source) { 61 | const result = this._comments.reduce((acc, comment) => { 62 | let lines = comment.split('\n'); 63 | if (comment.match(/^\s*\*\s*@external\s+/im)) { 64 | lines = lines.map((line) => (line.match(this._importExpression) ? ' * ' : line)); 65 | } else { 66 | lines = lines.map(() => ''); 67 | } 68 | 69 | return acc.replace(comment, lines.join('\n')); 70 | }, source); 71 | 72 | this._comments = []; 73 | return result; 74 | } 75 | } 76 | 77 | module.exports.TypedefImports = TypedefImports; 78 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /* eslint-disable no-new */ 3 | const { EventEmitter } = require('events'); 4 | const jsdocEnv = require('jsdoc/lib/jsdoc/env'); 5 | const jsdocTemplateHelper = require('jsdoc/lib/jsdoc/util/templateHelper'); 6 | const { EVENT_NAMES } = require('./constants'); 7 | const features = require('./features'); 8 | 9 | /** 10 | * @callback CommentsTraverseFn 11 | * @param {string} comment 12 | * @ignore 13 | */ 14 | 15 | /** 16 | * Finds all the JSDoc comments on a source and _walks_ them by calling the traverse 17 | * function. 18 | * 19 | * @param {string} source The code to analyze. 20 | * @param {CommentsTraverseFn} fn The function that will be called for each comment. 21 | * @ignore 22 | */ 23 | const traverseComments = (source, fn) => { 24 | const regex = /\/\*\*\s*(?:[^\*]|\*[^\/])*\*\//g; 25 | let match = regex.exec(source); 26 | while (match) { 27 | const [comment] = match; 28 | if (!comment.match(/\s*\* @ignore\s*\*/i)) { 29 | fn(comment); 30 | } 31 | match = regex.exec(source); 32 | } 33 | }; 34 | 35 | /** 36 | * The plugin options. 37 | * 38 | * @type {TSUtilsOptions} 39 | * @ignore 40 | */ 41 | const options = { 42 | typedefImports: true, 43 | typeOfTypes: true, 44 | extendTypes: true, 45 | modulesOnMemberOf: true, 46 | modulesTypesShortName: true, 47 | parentTag: true, 48 | removeTaggedBlocks: true, 49 | removeTags: true, 50 | typeScriptUtilityTypes: true, 51 | tagsReplacement: null, 52 | ...(jsdocEnv.conf.tsUtils || {}), 53 | }; 54 | 55 | /** 56 | * @type {EventEmitter} 57 | * @ignore 58 | */ 59 | const events = new EventEmitter(); 60 | 61 | // Load the features.. 62 | if (options.removeTaggedBlocks) { 63 | new features.RemoveTaggedBlocks(events, EVENT_NAMES); 64 | } 65 | 66 | if (options.removeTags) { 67 | new features.RemoveTags(events, EVENT_NAMES); 68 | } 69 | 70 | if (options.typedefImports) { 71 | new features.TypedefImports(events, EVENT_NAMES); 72 | } 73 | 74 | if (options.typeOfTypes) { 75 | new features.TypeOfTypes(events, EVENT_NAMES); 76 | } 77 | 78 | if (options.extendTypes) { 79 | new features.ExtendTypes(events, EVENT_NAMES); 80 | } 81 | 82 | if (options.modulesOnMemberOf) { 83 | new features.ModulesOnMemberOf(events, EVENT_NAMES); 84 | } 85 | 86 | if (options.modulesTypesShortName) { 87 | new features.ModulesTypesShortName(events, jsdocTemplateHelper, EVENT_NAMES); 88 | } 89 | 90 | if (options.parentTag) { 91 | new features.TagsReplacement({ parent: 'memberof' }, events, EVENT_NAMES); 92 | } 93 | 94 | if (options.typeScriptUtilityTypes) { 95 | new features.TSUtilitiesTypes(events, EVENT_NAMES); 96 | } 97 | 98 | if (options.tagsReplacement && Object.keys(options.tagsReplacement).length) { 99 | new features.TagsReplacement(options.tagsReplacement, events, EVENT_NAMES); 100 | } 101 | /** 102 | * Export all the loaded optiones. 103 | * 104 | * @type {TSUtilsOptions} 105 | * @ignore 106 | */ 107 | module.exports.options = options; 108 | /** 109 | * Export the handlers for JSDoc. 110 | * 111 | * @type {JSDocPluginHandlers} 112 | * @ignore 113 | */ 114 | module.exports.handlers = { 115 | parseBegin(event) { 116 | events.emit(EVENT_NAMES.parseBegin, event); 117 | }, 118 | beforeParse(event) { 119 | const { source, filename } = event; 120 | traverseComments(source, (comment) => 121 | events.emit(EVENT_NAMES.newComment, comment, filename), 122 | ); 123 | 124 | // eslint-disable-next-line no-param-reassign 125 | event.source = events 126 | .listeners(EVENT_NAMES.commentsReady) 127 | .reduce((acc, listener) => listener(acc, filename), source); 128 | }, 129 | }; 130 | -------------------------------------------------------------------------------- /src/typedef.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('events').EventEmitter} EventEmitter 3 | * @external EventEmitter 4 | * @see https://nodejs.org/api/events.html 5 | */ 6 | 7 | /** 8 | * @typedef {import('jsdoc/lib/jsdoc/util/templateHelper')} JSDocTemplateHelper 9 | * @external JSDocTemplateHelper 10 | * @see https://github.com/jsdoc/jsdoc/blob/3.5.5/lib/jsdoc/util/templateHelper.js 11 | */ 12 | 13 | /* eslint-disable jsdoc/valid-types, max-len */ 14 | /** 15 | * @typedef {import('jsdoc/lib/jsdoc/util/templateHelper')['registerLink']} 16 | * JSDocTemplateHelperRegisterLink 17 | * 18 | * @prettierignore 19 | */ 20 | /* eslint-enable jsdoc/valid-types */ 21 | 22 | /** 23 | * @typedef {import('./constants').EventNames} EventNames 24 | */ 25 | 26 | /** 27 | * @typedef {Object} TSUtilsOptions 28 | * @property {boolean} typedefImports 29 | * Whether or not to enable the feature that removes `typedef` statements that use 30 | * `import`. 31 | * Default `true`. 32 | * @property {boolean} typeOfTypes 33 | * Whether or not to enable the feature that replaces `{typeof T}` with `{Class.}`. 34 | * Default `true`. 35 | * @property {boolean} extendTypes 36 | * Whether or not to enable the feature that allows intersections to be reformatted. 37 | * Default `true`. 38 | * @property {boolean} removeTaggedBlocks 39 | * Whether or not to enable the feature that removes blocks that use the `@jsdoc-remove` 40 | * tag. 41 | * @property {boolean} removeTags 42 | * Whether or not to enable the feature that removes tags that follow a 43 | * `@jsdoc-remove-next-tag` tag. 44 | * @property {boolean} modulesOnMemberOf 45 | * Whether or not to enable the feature that fixes modules' paths on `memeberof` so they 46 | * can use dot notation. Default `true`. 47 | * @property {boolean} modulesTypesShortName 48 | * Whether or not to register modules types without the module path too. Default `true`. 49 | * @property {boolean} parentTag 50 | * Whether or not to transform all `parent` tags into `memberof`. Default `true`. 51 | * @property {boolean} typeScriptUtilityTypes 52 | * Whether or not to add the external utility types from TypeScript. Default `true`. 53 | * @property {?Object.} tagsReplacement 54 | * A dictionary of tags to replace, they keys are the tags being used and the values the 55 | * tag that should be used. Default `null`. 56 | */ 57 | 58 | /** 59 | * @typedef {Object} JSDocParseBeginEventPayload 60 | * @property {string[]} sourcefiles The list of files JSDoc will parse. 61 | * @ignore 62 | */ 63 | 64 | /** 65 | * @callback JSDocParseBeginHandler 66 | * @param {JSDocParseBeginEventPayload} event The JSDoc event information. 67 | * @ignore 68 | */ 69 | 70 | /** 71 | * @typedef {Object} JSDocBeforeParseEventPayload 72 | * @property {string} source The source code of the file that is going to be parsed. 73 | * @property {string} filename The absolute path to the file that is going to be parsed. 74 | * @ignore 75 | */ 76 | 77 | /** 78 | * @callback JSDocBeforeParseHandler 79 | * @param {JSDocBeforeParseEventPayload} event The JSDoc event information. 80 | * @ignore 81 | */ 82 | 83 | /** 84 | * @typedef {Object} JSDocPluginHandlers 85 | * @property {JSDocParseBeginHandler} parseBegin Called before parsing the files. 86 | * @property {JSDocBeforeParseHandler} beforeParse Called before parsing a single file. 87 | * @ignore 88 | */ 89 | -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "plugins": ["@homer0"], 4 | "extends": ["plugin:@homer0/jest-with-prettier"] 5 | } 6 | -------------------------------------------------------------------------------- /tests/features/extendTypes.test.js: -------------------------------------------------------------------------------- 1 | jest.unmock('../../src/features/extendTypes'); 2 | const { ExtendTypes } = require('../../src/features/extendTypes'); 3 | const { EVENT_NAMES } = require('../../src/constants'); 4 | 5 | describe('features:extendTypes', () => { 6 | it('should register the listeners when instantiated', () => { 7 | // Given 8 | const events = { 9 | on: jest.fn(), 10 | }; 11 | let sut = null; 12 | // When 13 | sut = new ExtendTypes(events, EVENT_NAMES); 14 | // Then 15 | expect(sut).toBeInstanceOf(ExtendTypes); 16 | expect(events.on).toHaveBeenCalledTimes(2); 17 | expect(events.on).toHaveBeenCalledWith(EVENT_NAMES.newComment, expect.any(Function)); 18 | expect(events.on).toHaveBeenCalledWith(EVENT_NAMES.commentsReady, expect.any(Function)); 19 | }); 20 | 21 | it('should ignore comments that don\'t extend types nor use intersection', () => { 22 | // Given 23 | const comment = [ 24 | '/**', 25 | ' * @typedef {Daughter} Rosario', 26 | ' * @typedef {Daughter} Pilar', 27 | ' */', 28 | ].join('\n'); 29 | const source = `${comment} Something`; 30 | const events = { 31 | on: jest.fn(), 32 | }; 33 | let sut = null; 34 | let onComment = null; 35 | let onCommentsReady = null; 36 | let result = null; 37 | // When 38 | sut = new ExtendTypes(events, EVENT_NAMES); 39 | [[, onComment], [, onCommentsReady]] = events.on.mock.calls; 40 | onComment(comment); 41 | result = onCommentsReady(source); 42 | // Then 43 | expect(sut).toBeInstanceOf(ExtendTypes); // to avoid `no-new`. 44 | expect(result).toBe(source); 45 | }); 46 | 47 | it('should transform an itersection into a union', () => { 48 | // Given 49 | const firstType = 'Object'; 50 | const secondType = 'SomeOtherType'; 51 | const definitionName = 'Child'; 52 | const comment = [ 53 | '/**', 54 | ` * @typedef {${firstType} & ${secondType}} ${definitionName}`, 55 | ' */', 56 | ].join('\n'); 57 | const content = ' Some other code'; 58 | const source = `${comment}${content}`; 59 | const events = { 60 | on: jest.fn(), 61 | }; 62 | let sut = null; 63 | let onComment = null; 64 | let onCommentsReady = null; 65 | let result = null; 66 | const newComment = [ 67 | '/**', 68 | ` * @typedef {${firstType}|${secondType}} ${definitionName}`, 69 | ' */', 70 | ].join('\n'); 71 | // When 72 | sut = new ExtendTypes(events, EVENT_NAMES); 73 | [[, onComment], [, onCommentsReady]] = events.on.mock.calls; 74 | onComment(comment); 75 | result = onCommentsReady(source); 76 | // Then 77 | expect(sut).toBeInstanceOf(ExtendTypes); // to avoid `no-new`. 78 | expect(result).toBe(`${newComment}${content}`); 79 | }); 80 | 81 | it('should transform an intersection into an extension', () => { 82 | // Given 83 | const sharedLines = [ 84 | ' * @memberof module:people', 85 | ]; 86 | const extendedProperiesLines = [ 87 | ' * @property {number} name', 88 | ' * @property {number} age', 89 | ' * @property {number} height', 90 | ]; 91 | const extendedType = 'Human'; 92 | const baseType = 'Entity'; 93 | const comment = [ 94 | '/**', 95 | ` * @typedef {${baseType} & ${extendedType}Properties} ${extendedType}`, 96 | ...sharedLines, 97 | ' */', 98 | ].join('\n'); 99 | const propertiesLines = [ 100 | '/**', 101 | ` * @typedef {Object} ${extendedType}Properties`, 102 | ...extendedProperiesLines, 103 | ...sharedLines, 104 | ` * @augments ${extendedType}`, 105 | ' */', 106 | ]; 107 | const propertiesComment = propertiesLines.join('\n'); 108 | const content = 'Some other code'; 109 | const source = [ 110 | comment, 111 | propertiesComment, 112 | content, 113 | ].join('\n'); 114 | const events = { 115 | on: jest.fn(), 116 | }; 117 | let sut = null; 118 | let onComment = null; 119 | let onCommentsReady = null; 120 | let result = null; 121 | const newComment = [ 122 | '/**', 123 | ` * @typedef {${baseType}} ${extendedType}`, 124 | ...sharedLines, 125 | ...extendedProperiesLines, 126 | ' */', 127 | ].join('\n'); 128 | const emptyBlock = new Array(propertiesLines.length - extendedProperiesLines.length) 129 | .fill('') 130 | .join('\n'); 131 | const expectedResult = [ 132 | newComment, 133 | emptyBlock, 134 | content, 135 | ].join('\n'); 136 | // When 137 | sut = new ExtendTypes(events, EVENT_NAMES); 138 | [[, onComment], [, onCommentsReady]] = events.on.mock.calls; 139 | onComment(comment); 140 | onComment(propertiesComment); 141 | result = onCommentsReady(source); 142 | // Then 143 | expect(sut).toBeInstanceOf(ExtendTypes); // to avoid `no-new`. 144 | expect(result).toBe(expectedResult); 145 | }); 146 | 147 | it('should transform an intersection (multiline) into an extension', () => { 148 | // Given 149 | const sharedLines = [ 150 | ' * @memberof module:people', 151 | ]; 152 | const extendedProperiesLines = [ 153 | ' * @property {number} name', 154 | ' * @property {number} age', 155 | ' * @property {number} height', 156 | ]; 157 | const extendedType = 'Human'; 158 | const baseType = 'Entity'; 159 | const comment = [ 160 | '/**', 161 | ` * @typedef {${baseType} & ${extendedType}Properties}`, 162 | ` * ${extendedType}`, 163 | ...sharedLines, 164 | ' */', 165 | ].join('\n'); 166 | const propertiesLines = [ 167 | '/**', 168 | ` * @typedef {Object} ${extendedType}Properties`, 169 | ...extendedProperiesLines, 170 | ...sharedLines, 171 | ` * @augments ${extendedType}`, 172 | ' */', 173 | ]; 174 | const propertiesComment = propertiesLines.join('\n'); 175 | const content = 'Some other code'; 176 | const source = [ 177 | comment, 178 | propertiesComment, 179 | content, 180 | ].join('\n'); 181 | const events = { 182 | on: jest.fn(), 183 | }; 184 | let sut = null; 185 | let onComment = null; 186 | let onCommentsReady = null; 187 | let result = null; 188 | const newComment = [ 189 | '/**', 190 | ` * @typedef {${baseType}}`, 191 | ` * ${extendedType}`, 192 | ...sharedLines, 193 | ...extendedProperiesLines, 194 | ' */', 195 | ].join('\n'); 196 | const emptyBlock = new Array(propertiesLines.length - extendedProperiesLines.length) 197 | .fill('') 198 | .join('\n'); 199 | const expectedResult = [ 200 | newComment, 201 | emptyBlock, 202 | content, 203 | ].join('\n'); 204 | // When 205 | sut = new ExtendTypes(events, EVENT_NAMES); 206 | [[, onComment], [, onCommentsReady]] = events.on.mock.calls; 207 | onComment(comment); 208 | onComment(propertiesComment); 209 | result = onCommentsReady(source); 210 | // Then 211 | expect(sut).toBeInstanceOf(ExtendTypes); // to avoid `no-new`. 212 | expect(result).toBe(expectedResult); 213 | }); 214 | }); 215 | -------------------------------------------------------------------------------- /tests/features/modulesOnMemberOf.test.js: -------------------------------------------------------------------------------- 1 | jest.unmock('../../src/features/modulesOnMemberOf'); 2 | const { ModulesOnMemberOf } = require('../../src/features/modulesOnMemberOf'); 3 | const { EVENT_NAMES } = require('../../src/constants'); 4 | 5 | describe('features:modulesOnMemberOf', () => { 6 | it('should register the listener when instantiated', () => { 7 | // Given 8 | const events = { 9 | on: jest.fn(), 10 | }; 11 | let sut = null; 12 | // When 13 | sut = new ModulesOnMemberOf(events, EVENT_NAMES); 14 | // Then 15 | expect(sut).toBeInstanceOf(ModulesOnMemberOf); 16 | expect(events.on).toHaveBeenCalledTimes(1); 17 | expect(events.on).toHaveBeenCalledWith(EVENT_NAMES.commentsReady, expect.any(Function)); 18 | }); 19 | 20 | it('should fix the modules\' paths on memberof tags that use dot notation', () => { 21 | // Given 22 | const fixture = [ 23 | { 24 | comment: '@memberof module.node/shared', 25 | expected: '@memberof module:node/shared', 26 | }, 27 | { 28 | comment: '@memberof module:node/utils', 29 | expected: '@memberof module:node/utils', 30 | }, 31 | { 32 | comment: '@memberof! module.node/shared', 33 | expected: '@memberof! module:node/shared', 34 | }, 35 | { 36 | comment: '@memberof! module:node/utils', 37 | expected: '@memberof! module:node/utils', 38 | }, 39 | { 40 | comment: '@memberof module.exports', 41 | expected: '@memberof module.exports', 42 | }, 43 | { 44 | comment: '@memberof! module.exports', 45 | expected: '@memberof! module.exports', 46 | }, 47 | ]; 48 | const [comment, expected] = ['comment', 'expected'].map((type) => [ 49 | '/**', 50 | ...fixture.map((item) => ` * ${item[type]}`), 51 | ' */', 52 | ].join('\n')); 53 | const events = { 54 | on: jest.fn(), 55 | }; 56 | let sut = null; 57 | let onCommentsReady = null; 58 | let result = null; 59 | // When 60 | sut = new ModulesOnMemberOf(events, EVENT_NAMES); 61 | [[, onCommentsReady]] = events.on.mock.calls; 62 | result = onCommentsReady(comment); 63 | // Then 64 | expect(sut).toBeInstanceOf(ModulesOnMemberOf); 65 | expect(result).toBe(expected); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /tests/features/modulesTypesShortName.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('jsdoc/lib/jsdoc/util/templateHelper', () => ({})); 2 | jest.unmock('../../src/features/modulesTypesShortName'); 3 | const jsdocTemplateHelper = require('jsdoc/lib/jsdoc/util/templateHelper'); 4 | const { ModulesTypesShortName } = require('../../src/features/modulesTypesShortName'); 5 | const { EVENT_NAMES } = require('../../src/constants'); 6 | 7 | describe('features:modulesTypesShortName', () => { 8 | let registerLinkMock; 9 | 10 | beforeEach(() => { 11 | registerLinkMock = jest.fn(); 12 | jsdocTemplateHelper.registerLink = registerLinkMock; 13 | }); 14 | 15 | it('should register the listener when instantiated', () => { 16 | // Given 17 | const events = { 18 | on: jest.fn(), 19 | }; 20 | let sut = null; 21 | // When 22 | sut = new ModulesTypesShortName(events, jsdocTemplateHelper, EVENT_NAMES); 23 | // Then 24 | expect(sut).toBeInstanceOf(ModulesTypesShortName); 25 | expect(events.on).toHaveBeenCalledTimes(1); 26 | expect(events.on).toHaveBeenCalledWith(EVENT_NAMES.parseBegin, expect.any(Function)); 27 | }); 28 | 29 | it('should monkey-patch the register link from the templates helper', () => { 30 | // Given 31 | const events = { 32 | on: jest.fn(), 33 | }; 34 | let sut = null; 35 | let onParseBegin = null; 36 | let patchStatusBeforeListener = null; 37 | let patchStatusAfterListener = null; 38 | // When 39 | sut = new ModulesTypesShortName(events, jsdocTemplateHelper, EVENT_NAMES); 40 | patchStatusBeforeListener = jsdocTemplateHelper.registerLink.monkey === true; 41 | [[, onParseBegin]] = events.on.mock.calls; 42 | onParseBegin(); 43 | patchStatusAfterListener = jsdocTemplateHelper.registerLink.monkey === true; 44 | // Then 45 | expect(sut).toBeInstanceOf(ModulesTypesShortName); 46 | expect(patchStatusBeforeListener).toBe(false); 47 | expect(patchStatusAfterListener).toBe(true); 48 | }); 49 | 50 | it('should ignore links that are not for externals nor for modules', () => { 51 | // Given 52 | const events = { 53 | on: jest.fn(), 54 | }; 55 | const longname = 'SomeFileType'; 56 | const fileUrl = 'some-file-url'; 57 | let sut = null; 58 | let onParseBegin = null; 59 | // When 60 | sut = new ModulesTypesShortName(events, jsdocTemplateHelper, EVENT_NAMES); 61 | [[, onParseBegin]] = events.on.mock.calls; 62 | onParseBegin(); 63 | jsdocTemplateHelper.registerLink(longname, fileUrl); 64 | // Then 65 | expect(sut).toBeInstanceOf(ModulesTypesShortName); 66 | expect(registerLinkMock).toHaveBeenCalledTimes(1); 67 | }); 68 | 69 | it('should register a short version for a module type', () => { 70 | // Given 71 | const events = { 72 | on: jest.fn(), 73 | }; 74 | const typeName = 'MyType'; 75 | const longname = `module:node.${typeName}`; 76 | const fileUrl = 'some-file-url'; 77 | let sut = null; 78 | let onParseBegin = null; 79 | // When 80 | sut = new ModulesTypesShortName(events, jsdocTemplateHelper, EVENT_NAMES); 81 | [[, onParseBegin]] = events.on.mock.calls; 82 | onParseBegin(); 83 | jsdocTemplateHelper.registerLink(longname, fileUrl); 84 | // Then 85 | expect(sut).toBeInstanceOf(ModulesTypesShortName); 86 | expect(registerLinkMock).toHaveBeenCalledTimes(2); 87 | expect(registerLinkMock).toHaveBeenCalledWith(longname, fileUrl); 88 | expect(registerLinkMock).toHaveBeenCalledWith(typeName, fileUrl); 89 | }); 90 | 91 | it('should register a short version for an external type', () => { 92 | // Given 93 | const events = { 94 | on: jest.fn(), 95 | }; 96 | const typeName = 'MyExternalType'; 97 | const longname = `external:${typeName}`; 98 | const fileUrl = 'some-file-url'; 99 | let sut = null; 100 | let onParseBegin = null; 101 | // When 102 | sut = new ModulesTypesShortName(events, jsdocTemplateHelper, EVENT_NAMES); 103 | [[, onParseBegin]] = events.on.mock.calls; 104 | onParseBegin(); 105 | jsdocTemplateHelper.registerLink(longname, fileUrl); 106 | // Then 107 | expect(sut).toBeInstanceOf(ModulesTypesShortName); 108 | expect(registerLinkMock).toHaveBeenCalledTimes(2); 109 | expect(registerLinkMock).toHaveBeenCalledWith(longname, fileUrl); 110 | expect(registerLinkMock).toHaveBeenCalledWith(typeName, fileUrl); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /tests/features/removeTaggedBlocks.test.js: -------------------------------------------------------------------------------- 1 | jest.unmock('../../src/features/removeTaggedBlocks'); 2 | const { RemoveTaggedBlocks } = require('../../src/features/removeTaggedBlocks'); 3 | const { EVENT_NAMES } = require('../../src/constants'); 4 | 5 | describe('features:removeTaggedBlocks', () => { 6 | it('should register the listeners when instantiated', () => { 7 | // Given 8 | const events = { 9 | on: jest.fn(), 10 | }; 11 | let sut = null; 12 | // When 13 | sut = new RemoveTaggedBlocks(events, EVENT_NAMES); 14 | // Then 15 | expect(sut).toBeInstanceOf(RemoveTaggedBlocks); 16 | expect(events.on).toHaveBeenCalledTimes(2); 17 | expect(events.on).toHaveBeenCalledWith(EVENT_NAMES.newComment, expect.any(Function)); 18 | expect(events.on).toHaveBeenCalledWith( 19 | EVENT_NAMES.commentsReady, 20 | expect.any(Function), 21 | ); 22 | }); 23 | 24 | it("should ignore a comment that doesn't have a jsdoc-remove tag", () => { 25 | // Given 26 | const comment = [ 27 | '/**', 28 | ' * @typedef {Daughter} Rosario', 29 | ' * @typedef {Daughter} Pilar', 30 | ' */', 31 | ].join('\n'); 32 | const source = `${comment} Something`; 33 | const events = { 34 | on: jest.fn(), 35 | }; 36 | let sut = null; 37 | let onComment = null; 38 | let onCommentsReady = null; 39 | let result = null; 40 | // When 41 | sut = new RemoveTaggedBlocks(events, EVENT_NAMES); 42 | [[, onComment], [, onCommentsReady]] = events.on.mock.calls; 43 | onComment(comment); 44 | result = onCommentsReady(source); 45 | // Then 46 | expect(sut).toBeInstanceOf(RemoveTaggedBlocks); // to avoid `no-new`. 47 | expect(result).toBe(source); 48 | }); 49 | 50 | it('should remove a block with a jsdoc-remove tag', () => { 51 | // Given 52 | const commentLines = [ 53 | '/**', 54 | ' * @typedef {Daughter} Rosario', 55 | ' * @typedef {Daughter} Pilar', 56 | ' * @jsdoc-remove', 57 | ' */', 58 | ]; 59 | const comment = commentLines.join('\n'); 60 | const content = ' Some other code'; 61 | const source = `${comment}${content}`; 62 | const events = { 63 | on: jest.fn(), 64 | }; 65 | let sut = null; 66 | let onComment = null; 67 | let onCommentsReady = null; 68 | const emptyBlock = commentLines.map(() => '').join('\n'); 69 | let result = null; 70 | // When 71 | sut = new RemoveTaggedBlocks(events, EVENT_NAMES); 72 | [[, onComment], [, onCommentsReady]] = events.on.mock.calls; 73 | onComment(comment); 74 | result = onCommentsReady(source); 75 | // Then 76 | expect(sut).toBeInstanceOf(RemoveTaggedBlocks); // to avoid `no-new`. 77 | expect(result).toBe(`${emptyBlock}${content}`); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /tests/features/removeTags.test.js: -------------------------------------------------------------------------------- 1 | jest.unmock('../../src/features/removeTags'); 2 | const { RemoveTags } = require('../../src/features/removeTags'); 3 | const { EVENT_NAMES } = require('../../src/constants'); 4 | 5 | describe('features:removeTags', () => { 6 | it('should register the listeners when instantiated', () => { 7 | // Given 8 | const events = { 9 | on: jest.fn(), 10 | }; 11 | let sut = null; 12 | // When 13 | sut = new RemoveTags(events, EVENT_NAMES); 14 | // Then 15 | expect(sut).toBeInstanceOf(RemoveTags); 16 | expect(events.on).toHaveBeenCalledTimes(2); 17 | expect(events.on).toHaveBeenCalledWith(EVENT_NAMES.newComment, expect.any(Function)); 18 | expect(events.on).toHaveBeenCalledWith( 19 | EVENT_NAMES.commentsReady, 20 | expect.any(Function), 21 | ); 22 | }); 23 | 24 | it("should ignore a comment that doesn't have a jsdoc-remove-next-tag tag", () => { 25 | // Given 26 | const comment = [ 27 | '/**', 28 | ' * @typedef {Daughter} Rosario', 29 | ' * @typedef {Daughter} Pilar', 30 | ' */', 31 | ].join('\n'); 32 | const source = `${comment} Something`; 33 | const events = { 34 | on: jest.fn(), 35 | }; 36 | let sut = null; 37 | let onComment = null; 38 | let onCommentsReady = null; 39 | let result = null; 40 | // When 41 | sut = new RemoveTags(events, EVENT_NAMES); 42 | [[, onComment], [, onCommentsReady]] = events.on.mock.calls; 43 | onComment(comment); 44 | result = onCommentsReady(source); 45 | // Then 46 | expect(sut).toBeInstanceOf(RemoveTags); // to avoid `no-new`. 47 | expect(result).toBe(source); 48 | }); 49 | 50 | it('should remove a tag', () => { 51 | // Given 52 | const commentLinesStart = ['/**']; 53 | const commentLinesToRemove = [ 54 | ' * @jsdoc-remove-next-tag', 55 | ' * @typedef {Something} else', 56 | ]; 57 | const commentLinesEnd = [ 58 | ' * @typedef {Daughter} Rosario', 59 | ' * @typedef {Daughter} Pilar', 60 | ' */', 61 | ]; 62 | const comment = [ 63 | ...commentLinesStart, 64 | ...commentLinesToRemove, 65 | ...commentLinesEnd, 66 | ].join('\n'); 67 | const content = ' Some other code'; 68 | const source = `${comment}${content}`; 69 | const events = { 70 | on: jest.fn(), 71 | }; 72 | let sut = null; 73 | let onComment = null; 74 | let onCommentsReady = null; 75 | const newComment = [...commentLinesStart, ...commentLinesEnd].join('\n'); 76 | let result = null; 77 | // When 78 | sut = new RemoveTags(events, EVENT_NAMES); 79 | [[, onComment], [, onCommentsReady]] = events.on.mock.calls; 80 | onComment(comment); 81 | result = onCommentsReady(source); 82 | // Then 83 | expect(sut).toBeInstanceOf(RemoveTags); // to avoid `no-new`. 84 | expect(result).toBe(`${newComment}${content}`); 85 | }); 86 | 87 | it('should remove multiple tags', () => { 88 | // Given 89 | const comment = [ 90 | '/**', 91 | ' * @jsdoc-remove-next-tag', 92 | " * @typedef {JQuery.AjaxSettings['success']} JQueryOnSuccess", 93 | ' * @external JQueryOnSuccess', 94 | ' * @see {@link http://api.jquery.com/jQuery.ajax/#success}', 95 | ' * @jsdoc-remove-next-tag', 96 | " * @typedef {JQuery.AjaxSettings['error']} JQueryOnError", 97 | ' * @external JQueryOnError', 98 | ' * @see {@link http://api.jquery.com/jQuery.ajax/#error}', 99 | ' * @jsdoc-remove-next-tag', 100 | ' * @typedef {{(param1: string, param2: number) => string}} SomeFn', 101 | ' */', 102 | ].join('\n'); 103 | const newComment = [ 104 | '/**', 105 | ' * @external JQueryOnSuccess', 106 | ' * @see {@link http://api.jquery.com/jQuery.ajax/#success}', 107 | ' * @external JQueryOnError', 108 | ' * @see {@link http://api.jquery.com/jQuery.ajax/#error}', 109 | ' */', 110 | ].join('\n'); 111 | const content = ' Some other code'; 112 | const source = `${comment}${content}`; 113 | const events = { 114 | on: jest.fn(), 115 | }; 116 | let sut = null; 117 | let onComment = null; 118 | let onCommentsReady = null; 119 | let result = null; 120 | // When 121 | sut = new RemoveTags(events, EVENT_NAMES); 122 | [[, onComment], [, onCommentsReady]] = events.on.mock.calls; 123 | onComment(comment); 124 | result = onCommentsReady(source); 125 | // Then 126 | expect(sut).toBeInstanceOf(RemoveTags); // to avoid `no-new`. 127 | expect(result).toBe(`${newComment}${content}`); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /tests/features/tagsReplacement.test.js: -------------------------------------------------------------------------------- 1 | jest.unmock('../../src/features/tagsReplacement'); 2 | const { TagsReplacement } = require('../../src/features/tagsReplacement'); 3 | const { EVENT_NAMES } = require('../../src/constants'); 4 | 5 | describe('features:tagsReplacement', () => { 6 | it('should register the listener when instantiated', () => { 7 | // Given 8 | const dictionary = {}; 9 | const events = { 10 | on: jest.fn(), 11 | }; 12 | let sut = null; 13 | // When 14 | sut = new TagsReplacement(dictionary, events, EVENT_NAMES); 15 | // Then 16 | expect(sut).toBeInstanceOf(TagsReplacement); 17 | expect(events.on).toHaveBeenCalledTimes(1); 18 | expect(events.on).toHaveBeenCalledWith(EVENT_NAMES.commentsReady, expect.any(Function)); 19 | }); 20 | 21 | it('should replace a dictionary of tags on a file', () => { 22 | // Given 23 | const fixture = [ 24 | { 25 | original: 'parent', 26 | replacement: 'memberof', 27 | comment: 'module:node/shared', 28 | }, 29 | { 30 | original: 'extends', 31 | replacement: 'augments', 32 | comment: 'SomeType', 33 | }, 34 | ]; 35 | const [comment, expected] = ['original', 'replacement'].map((type) => [ 36 | '/**', 37 | ...fixture.map((item) => ` * @${item[type]} ${item.comment}`), 38 | ' */', 39 | ].join('\n')); 40 | const extraContent = [ 41 | 'Some text with @parent in the middle', 42 | 'or a single line /* @extends Woo */', 43 | '/**', 44 | ' * @type {string}', 45 | ' */', 46 | 'const toTestAnotherBlock = \'\';', 47 | ].join('\n'); 48 | const dictionary = fixture.reduce( 49 | (acc, item) => ({ ...acc, [item.original]: item.replacement }), 50 | {}, 51 | ); 52 | const events = { 53 | on: jest.fn(), 54 | }; 55 | let sut = null; 56 | let onCommentsReady = null; 57 | let result = null; 58 | // When 59 | sut = new TagsReplacement(dictionary, events, EVENT_NAMES); 60 | [[, onCommentsReady]] = events.on.mock.calls; 61 | result = onCommentsReady(`${comment}\n${extraContent}`); 62 | // Then 63 | expect(sut).toBeInstanceOf(TagsReplacement); 64 | expect(result).toBe(`${expected}\n${extraContent}`); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /tests/features/tsUtilityTypes.test.js: -------------------------------------------------------------------------------- 1 | jest.unmock('../../src/features/tsUtilityTypes'); 2 | const { TSUtilitiesTypes } = require('../../src/features/tsUtilityTypes'); 3 | const { EVENT_NAMES } = require('../../src/constants'); 4 | 5 | describe('features:tsUtilityTypes', () => { 6 | const EXTERNALS_COMMENT = [ 7 | '/**', 8 | ' * @external Partial', 9 | ' * @see https://www.typescriptlang.org/docs/handbook/utility-types.html#partialt', 10 | ' */\n', 11 | '/**', 12 | ' * @external Readonly', 13 | ' * @see https://www.typescriptlang.org/docs/handbook/utility-types.html#readonlyt', 14 | ' */\n', 15 | '/**', 16 | ' * @external Record', 17 | ' * @see https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkt', 18 | ' */\n', 19 | '/**', 20 | ' * @external Pick', 21 | ' * @see https://www.typescriptlang.org/docs/handbook/utility-types.html#picktk', 22 | ' */\n', 23 | '/**', 24 | ' * @external Omit', 25 | ' * @see https://www.typescriptlang.org/docs/handbook/utility-types.html#omittk', 26 | ' */\n', 27 | '/**', 28 | ' * @external Exclude', 29 | ' * @see https://www.typescriptlang.org/docs/handbook/utility-types.html#excludetu', 30 | ' */\n', 31 | '/**', 32 | ' * @external Extract', 33 | ' * @see https://www.typescriptlang.org/docs/handbook/utility-types.html#extracttu', 34 | ' */\n', 35 | '/**', 36 | ' * @external NonNullable', 37 | ' * @see https://www.typescriptlang.org/docs/handbook/utility-types.html#nonnullablet', 38 | ' */\n', 39 | '/**', 40 | ' * @external Parameters', 41 | ' * @see https://www.typescriptlang.org/docs/handbook/utility-types.html#parameterst', 42 | ' */\n', 43 | '/**', 44 | ' * @external ConstructorParameters', 45 | ' * @see https://www.typescriptlang.org/docs/handbook/utility-types.html#constructorparameterst', 46 | ' */\n', 47 | '/**', 48 | ' * @external ReturnType', 49 | ' * @see https://www.typescriptlang.org/docs/handbook/utility-types.html#returntypet', 50 | ' */\n', 51 | '/**', 52 | ' * @external InstanceType', 53 | ' * @see https://www.typescriptlang.org/docs/handbook/utility-types.html#instancetypet', 54 | ' */\n', 55 | '/**', 56 | ' * @external Required', 57 | ' * @see https://www.typescriptlang.org/docs/handbook/utility-types.html#requiredt', 58 | ' */\n', 59 | '/**', 60 | ' * @external ThisParameterType', 61 | ' * @see https://www.typescriptlang.org/docs/handbook/utility-types.html#thisparametertype', 62 | ' */\n', 63 | '/**', 64 | ' * @external OmitThisParameter', 65 | ' * @see https://www.typescriptlang.org/docs/handbook/utility-types.html#omitthisparameter', 66 | ' */\n', 67 | '/**', 68 | ' * @external ThisType', 69 | ' * @see https://www.typescriptlang.org/docs/handbook/utility-types.html#thistypet', 70 | ' */\n', 71 | ].join('\n'); 72 | 73 | it('should register the listeners when instantiated', () => { 74 | // Given 75 | const events = { 76 | on: jest.fn(), 77 | }; 78 | let sut = null; 79 | // When 80 | sut = new TSUtilitiesTypes(events, EVENT_NAMES); 81 | // Then 82 | expect(sut).toBeInstanceOf(TSUtilitiesTypes); 83 | expect(events.on).toHaveBeenCalledTimes(2); 84 | expect(events.on).toHaveBeenCalledWith(EVENT_NAMES.parseBegin, expect.any(Function)); 85 | expect(events.on).toHaveBeenCalledWith(EVENT_NAMES.commentsReady, expect.any(Function)); 86 | }); 87 | 88 | it('should add the types on a typedef.js file', () => { 89 | // Given 90 | const otherFile = { 91 | path: 'some/other/path/index.js', 92 | contents: 'throw new Error(\'we are doomed\');', 93 | }; 94 | const typedefFile = { 95 | path: 'some/path/typedef.js', 96 | contents: '/** @typedef {Object} Something */', 97 | }; 98 | const files = [otherFile, typedefFile]; 99 | const events = { 100 | on: jest.fn(), 101 | }; 102 | let sut = null; 103 | let onParseBegin = null; 104 | let onCommentsReady = null; 105 | let results = null; 106 | // When 107 | sut = new TSUtilitiesTypes(events, EVENT_NAMES); 108 | [[, onParseBegin], [, onCommentsReady]] = events.on.mock.calls; 109 | onParseBegin({ 110 | sourcefiles: files.map((file) => file.path), 111 | }); 112 | results = files.map((file) => onCommentsReady( 113 | file.contents, 114 | file.path, 115 | )); 116 | // Then 117 | expect(sut).toBeInstanceOf(TSUtilitiesTypes); 118 | expect(results).toEqual([ 119 | otherFile.contents, 120 | `${typedefFile.contents}\n\n${EXTERNALS_COMMENT}`, 121 | ]); 122 | }); 123 | 124 | it('should add the types on the first file if there\'s no typedef.js', () => { 125 | // Given 126 | const typedefFile = { 127 | path: 'some/path/index.js', 128 | contents: '/** @typedef {Object} Something */', 129 | }; 130 | const otherFile = { 131 | path: 'some/other/path/index.js', 132 | contents: 'throw new Error(\'we are doomed\');', 133 | }; 134 | const files = [typedefFile, otherFile]; 135 | const events = { 136 | on: jest.fn(), 137 | }; 138 | let sut = null; 139 | let onParseBegin = null; 140 | let onCommentsReady = null; 141 | let results = null; 142 | // When 143 | sut = new TSUtilitiesTypes(events, EVENT_NAMES); 144 | [[, onParseBegin], [, onCommentsReady]] = events.on.mock.calls; 145 | onParseBegin({ 146 | sourcefiles: files.map((file) => file.path), 147 | }); 148 | results = files.map((file) => onCommentsReady( 149 | file.contents, 150 | file.path, 151 | )); 152 | // Then 153 | expect(sut).toBeInstanceOf(TSUtilitiesTypes); 154 | expect(results).toEqual([ 155 | `${typedefFile.contents}\n\n${EXTERNALS_COMMENT}`, 156 | otherFile.contents, 157 | ]); 158 | }); 159 | }); 160 | -------------------------------------------------------------------------------- /tests/features/typeOfTypes.test.js: -------------------------------------------------------------------------------- 1 | jest.unmock('../../src/features/typeOfTypes'); 2 | const { TypeOfTypes } = require('../../src/features/typeOfTypes'); 3 | const { EVENT_NAMES } = require('../../src/constants'); 4 | 5 | describe('features:typeOfTypes', () => { 6 | it('should register the listeners when instantiated', () => { 7 | // Given 8 | const events = { 9 | on: jest.fn(), 10 | }; 11 | let sut = null; 12 | // When 13 | sut = new TypeOfTypes(events, EVENT_NAMES); 14 | // Then 15 | expect(sut).toBeInstanceOf(TypeOfTypes); 16 | expect(events.on).toHaveBeenCalledTimes(2); 17 | expect(events.on).toHaveBeenCalledWith(EVENT_NAMES.newComment, expect.any(Function)); 18 | expect(events.on).toHaveBeenCalledWith(EVENT_NAMES.commentsReady, expect.any(Function)); 19 | }); 20 | 21 | it('should ignore a comment that doesn\'t have a typedef with typeof', () => { 22 | // Given 23 | const comment = [ 24 | '/**', 25 | ' * @typedef {Daughter} Rosario', 26 | ' * @typedef {Daughter} Pilar', 27 | ' */', 28 | ].join('\n'); 29 | const source = `${comment} Something`; 30 | const events = { 31 | on: jest.fn(), 32 | }; 33 | let sut = null; 34 | let onComment = null; 35 | let onCommentsReady = null; 36 | let result = null; 37 | // When 38 | sut = new TypeOfTypes(events, EVENT_NAMES); 39 | [[, onComment], [, onCommentsReady]] = events.on.mock.calls; 40 | onComment(comment); 41 | result = onCommentsReady(source); 42 | // Then 43 | expect(sut).toBeInstanceOf(TypeOfTypes); // to avoid `no-new`. 44 | expect(result).toBe(source); 45 | }); 46 | 47 | it('should use Class. for a type with typeof on a typedef', () => { 48 | // Given 49 | const type = 'Daughter'; 50 | const commentLinesStart = ['/**']; 51 | const commentLineTypeOf = ` * @typedef {typeof ${type}} DaughterConstructor`; 52 | const commentLinesEnd = [ 53 | ' * @typedef {Daughter} Rosario', 54 | ' * @typedef {Daughter} Pilar', 55 | ' */', 56 | ]; 57 | const comment = [ 58 | ...commentLinesStart, 59 | commentLineTypeOf, 60 | ...commentLinesEnd, 61 | ] 62 | .join('\n'); 63 | const content = ' Some other code'; 64 | const source = `${comment}${content}`; 65 | const events = { 66 | on: jest.fn(), 67 | }; 68 | let sut = null; 69 | let onComment = null; 70 | let onCommentsReady = null; 71 | const newComment = [ 72 | ...commentLinesStart, 73 | ` * @typedef {Class.<${type}>} DaughterConstructor`, 74 | ...commentLinesEnd, 75 | ] 76 | .join('\n'); 77 | let result = null; 78 | // When 79 | sut = new TypeOfTypes(events, EVENT_NAMES); 80 | [[, onComment], [, onCommentsReady]] = events.on.mock.calls; 81 | onComment(comment); 82 | result = onCommentsReady(source); 83 | // Then 84 | expect(sut).toBeInstanceOf(TypeOfTypes); // to avoid `no-new`. 85 | expect(result).toBe(`${newComment}${content}`); 86 | }); 87 | 88 | it('should use Class. for a type with typeof on a function block', () => { 89 | // Given 90 | const type = 'Daughter'; 91 | const paramDescription = 'Some description'; 92 | const commentLinesStart = ['/**', 'Some function description', '']; 93 | const commentLineTypeOf = ` * @param {typeof ${type}} DaughterConstructor ${paramDescription}`; 94 | const commentLinesEnd = [ 95 | ` * @param {Daughter} Rosario ${paramDescription}`, 96 | ` * @param {Daughter} Pilar ${paramDescription}`, 97 | ' * @returns {string}', 98 | ' */', 99 | ]; 100 | const comment = [ 101 | ...commentLinesStart, 102 | commentLineTypeOf, 103 | ...commentLinesEnd, 104 | ] 105 | .join('\n'); 106 | const content = [ 107 | '', 108 | 'function doSometing(DaughterConstructor, Rosario, Pilar) {', 109 | ' return \'something\';', 110 | '}', 111 | ] 112 | .join('\n'); 113 | const source = `${comment}${content}`; 114 | const events = { 115 | on: jest.fn(), 116 | }; 117 | let sut = null; 118 | let onComment = null; 119 | let onCommentsReady = null; 120 | const newComment = [ 121 | ...commentLinesStart, 122 | ` * @param {Class.<${type}>} DaughterConstructor ${paramDescription}`, 123 | ...commentLinesEnd, 124 | ] 125 | .join('\n'); 126 | let result = null; 127 | // When 128 | sut = new TypeOfTypes(events, EVENT_NAMES); 129 | [[, onComment], [, onCommentsReady]] = events.on.mock.calls; 130 | onComment(comment); 131 | result = onCommentsReady(source); 132 | // Then 133 | expect(sut).toBeInstanceOf(TypeOfTypes); // to avoid `no-new`. 134 | expect(result).toBe(`${newComment}${content}`); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /tests/features/typedefImports.test.js: -------------------------------------------------------------------------------- 1 | jest.unmock('../../src/features/typedefImports'); 2 | const { TypedefImports } = require('../../src/features/typedefImports'); 3 | const { EVENT_NAMES } = require('../../src/constants'); 4 | 5 | describe('features:typedefImports', () => { 6 | it('should register the listeners when instantiated', () => { 7 | // Given 8 | const events = { 9 | on: jest.fn(), 10 | }; 11 | let sut = null; 12 | // When 13 | sut = new TypedefImports(events, EVENT_NAMES); 14 | // Then 15 | expect(sut).toBeInstanceOf(TypedefImports); 16 | expect(events.on).toHaveBeenCalledTimes(2); 17 | expect(events.on).toHaveBeenCalledWith(EVENT_NAMES.newComment, expect.any(Function)); 18 | expect(events.on).toHaveBeenCalledWith(EVENT_NAMES.commentsReady, expect.any(Function)); 19 | }); 20 | 21 | it('should ignore a comment that doesn\'t have a typedef import', () => { 22 | // Given 23 | const comment = [ 24 | '/**', 25 | ' * @typedef {Daughter} Rosario', 26 | ' * @typedef {Daughter} Pilar', 27 | ' */', 28 | ].join('\n'); 29 | const source = `${comment} Something`; 30 | const events = { 31 | on: jest.fn(), 32 | }; 33 | let sut = null; 34 | let onComment = null; 35 | let onCommentsReady = null; 36 | let result = null; 37 | // When 38 | sut = new TypedefImports(events, EVENT_NAMES); 39 | [[, onComment], [, onCommentsReady]] = events.on.mock.calls; 40 | onComment(comment); 41 | result = onCommentsReady(source); 42 | // Then 43 | expect(sut).toBeInstanceOf(TypedefImports); // to avoid `no-new`. 44 | expect(result).toBe(source); 45 | }); 46 | 47 | it('should remove a block with a typedef import', () => { 48 | // Given 49 | const commentLines = [ 50 | '/**', 51 | ' * @typedef {import("family").Daughter} Daughter', 52 | ' * @typedef {Daughter} Rosario', 53 | ' * @typedef {Daughter} Pilar', 54 | ' */', 55 | ]; 56 | const comment = commentLines.join('\n'); 57 | const content = ' Some other code'; 58 | const source = `${comment}${content}`; 59 | const events = { 60 | on: jest.fn(), 61 | }; 62 | let sut = null; 63 | let onComment = null; 64 | let onCommentsReady = null; 65 | const emptyBlock = commentLines.map(() => '').join('\n'); 66 | let result = null; 67 | // When 68 | sut = new TypedefImports(events, EVENT_NAMES); 69 | [[, onComment], [, onCommentsReady]] = events.on.mock.calls; 70 | onComment(comment); 71 | result = onCommentsReady(source); 72 | // Then 73 | expect(sut).toBeInstanceOf(TypedefImports); // to avoid `no-new`. 74 | expect(result).toBe(`${emptyBlock}${content}`); 75 | }); 76 | 77 | it('shouldn\'t remove the block if theres an @external tag', () => { 78 | // Given 79 | const commentLinesStart = ['/**']; 80 | const commentLinesImport = [' * @typedef {import("family").Daughter} Daughter']; 81 | const commentLinesEnd = [ 82 | ' * @external Daughter', 83 | ' * @see https://www.instagram.com/p/CAiy3zYg7Vx/', 84 | ' * @typedef {Daughter} Rosario', 85 | ' * @typedef {Daughter} Pilar', 86 | ' */', 87 | ]; 88 | const comment = [ 89 | ...commentLinesStart, 90 | ...commentLinesImport, 91 | ...commentLinesEnd, 92 | ] 93 | .join('\n'); 94 | const content = ' Some other code'; 95 | const source = `${comment}${content}`; 96 | const events = { 97 | on: jest.fn(), 98 | }; 99 | let sut = null; 100 | let onComment = null; 101 | let onCommentsReady = null; 102 | const newComment = [ 103 | ...commentLinesStart, 104 | ...commentLinesImport.map(() => ' * '), 105 | ...commentLinesEnd, 106 | ] 107 | .join('\n'); 108 | let result = null; 109 | // When 110 | sut = new TypedefImports(events, EVENT_NAMES); 111 | [[, onComment], [, onCommentsReady]] = events.on.mock.calls; 112 | onComment(comment); 113 | result = onCommentsReady(source); 114 | // Then 115 | expect(sut).toBeInstanceOf(TypedefImports); // to avoid `no-new`. 116 | expect(result).toBe(`${newComment}${content}`); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /tests/index.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('../src')} Plugin 3 | * @typedef {import('../src/features')} Features 4 | */ 5 | 6 | /** 7 | * @typedef {Object} EventEmitterMock 8 | * @property {Function} emit Emits events to the listeners. 9 | * @property {Function} listeners Gets the listeners for an event. 10 | */ 11 | 12 | /** 13 | * @typedef {Object} LoadedPlugin 14 | * @property {Plugin} plugin The loaded instance of the plugin. 15 | * @property {Features} features The dictionary of mocked features. 16 | * @property {EventEmitterMock} events The mock for the event emitter. 17 | */ 18 | 19 | jest.unmock('../src'); 20 | jest.mock('events'); 21 | jest.mock('jsdoc/lib/jsdoc/env', () => ({ 22 | conf: {}, 23 | })); 24 | jest.mock('jsdoc/lib/jsdoc/util/templateHelper', () => ({})); 25 | 26 | const jsdocTemplateHelper = require('jsdoc/lib/jsdoc/util/templateHelper'); 27 | const { EVENT_NAMES } = require('../src/constants'); 28 | 29 | describe('plugin', () => { 30 | /** 31 | * Since the JSDoc options are parsed when the modules are loaded, in order for the 32 | * tests to validate different configurations, this function will load new instances of 33 | * the plugin, 34 | * the JSDoc env module, the events module and the features. 35 | * 36 | * @param {?Partial} options The custom options for the plugin. 37 | * @param {?EventEmitterMock} eventsMock A custom mock for th event emitter 38 | * instance. 39 | * @returns {LoadedPlugin} 40 | */ 41 | const loadPlugin = (options = null, eventsMock = null) => { 42 | if (options) { 43 | // eslint-disable-next-line global-require 44 | const jsdocEnv = require('jsdoc/lib/jsdoc/env'); 45 | jsdocEnv.conf.tsUtils = options; 46 | } 47 | // eslint-disable-next-line global-require 48 | const { EventEmitter } = require('events'); 49 | const events = eventsMock || { 50 | emit: jest.fn(), 51 | listeners: jest.fn(), 52 | }; 53 | EventEmitter.mockImplementationOnce(() => events); 54 | // eslint-disable-next-line global-require 55 | const plugin = require('../src'); 56 | // eslint-disable-next-line global-require 57 | const features = require('../src/features'); 58 | return { 59 | plugin, 60 | features, 61 | events, 62 | }; 63 | }; 64 | 65 | beforeEach(() => { 66 | jest.resetModules(); 67 | }); 68 | 69 | it('should be loaded with the default options', () => { 70 | // Given 71 | let sut = null; 72 | let features = null; 73 | let events = null; 74 | // When 75 | ({ plugin: sut, features, events } = loadPlugin()); 76 | // Then 77 | expect(sut.handlers).toEqual({ 78 | parseBegin: expect.any(Function), 79 | beforeParse: expect.any(Function), 80 | }); 81 | expect(sut.options).toEqual({ 82 | typedefImports: true, 83 | typeOfTypes: true, 84 | extendTypes: true, 85 | modulesOnMemberOf: true, 86 | modulesTypesShortName: true, 87 | parentTag: true, 88 | removeTaggedBlocks: true, 89 | removeTags: true, 90 | typeScriptUtilityTypes: true, 91 | tagsReplacement: null, 92 | }); 93 | expect(features.TypedefImports).toHaveBeenCalledTimes(1); 94 | expect(features.TypedefImports).toHaveBeenCalledWith(events, EVENT_NAMES); 95 | expect(features.TypeOfTypes).toHaveBeenCalledTimes(1); 96 | expect(features.TypeOfTypes).toHaveBeenCalledWith(events, EVENT_NAMES); 97 | expect(features.ExtendTypes).toHaveBeenCalledTimes(1); 98 | expect(features.ExtendTypes).toHaveBeenCalledWith(events, EVENT_NAMES); 99 | expect(features.ModulesOnMemberOf).toHaveBeenCalledTimes(1); 100 | expect(features.ModulesOnMemberOf).toHaveBeenCalledWith(events, EVENT_NAMES); 101 | expect(features.ModulesTypesShortName).toHaveBeenCalledTimes(1); 102 | expect(features.ModulesTypesShortName).toHaveBeenCalledWith( 103 | events, 104 | jsdocTemplateHelper, 105 | EVENT_NAMES, 106 | ); 107 | expect(features.TagsReplacement).toHaveBeenCalledTimes(1); 108 | expect(features.TagsReplacement).toHaveBeenCalledWith( 109 | { parent: 'memberof' }, 110 | events, 111 | EVENT_NAMES, 112 | ); 113 | expect(features.RemoveTaggedBlocks).toHaveBeenCalledTimes(1); 114 | expect(features.RemoveTaggedBlocks).toHaveBeenCalledWith(events, EVENT_NAMES); 115 | expect(features.RemoveTags).toHaveBeenCalledTimes(1); 116 | expect(features.RemoveTags).toHaveBeenCalledWith(events, EVENT_NAMES); 117 | expect(features.TSUtilitiesTypes).toHaveBeenCalledTimes(1); 118 | expect(features.TSUtilitiesTypes).toHaveBeenCalledWith(events, EVENT_NAMES); 119 | }); 120 | 121 | it('should be loaded without the typedef imports feature', () => { 122 | // Given 123 | let sut = null; 124 | let features = null; 125 | // When 126 | ({ plugin: sut, features } = loadPlugin({ 127 | typedefImports: false, 128 | })); 129 | // Then 130 | expect(sut.options).toEqual({ 131 | typedefImports: false, 132 | typeOfTypes: true, 133 | extendTypes: true, 134 | modulesOnMemberOf: true, 135 | modulesTypesShortName: true, 136 | parentTag: true, 137 | removeTaggedBlocks: true, 138 | removeTags: true, 139 | typeScriptUtilityTypes: true, 140 | tagsReplacement: null, 141 | }); 142 | expect(features.TypedefImports).toHaveBeenCalledTimes(0); 143 | }); 144 | 145 | it('should be loaded without the typeof types feature', () => { 146 | // Given 147 | let sut = null; 148 | let features = null; 149 | // When 150 | ({ plugin: sut, features } = loadPlugin({ 151 | typeOfTypes: false, 152 | })); 153 | // Then 154 | expect(sut.options).toEqual({ 155 | typedefImports: true, 156 | typeOfTypes: false, 157 | extendTypes: true, 158 | modulesOnMemberOf: true, 159 | modulesTypesShortName: true, 160 | parentTag: true, 161 | removeTaggedBlocks: true, 162 | removeTags: true, 163 | typeScriptUtilityTypes: true, 164 | tagsReplacement: null, 165 | }); 166 | expect(features.TypeOfTypes).toHaveBeenCalledTimes(0); 167 | }); 168 | 169 | it('should be loaded without the extend types feature', () => { 170 | // Given 171 | let sut = null; 172 | let features = null; 173 | // When 174 | ({ plugin: sut, features } = loadPlugin({ 175 | extendTypes: false, 176 | })); 177 | // Then 178 | expect(sut.options).toEqual({ 179 | typedefImports: true, 180 | typeOfTypes: true, 181 | extendTypes: false, 182 | modulesOnMemberOf: true, 183 | modulesTypesShortName: true, 184 | parentTag: true, 185 | removeTaggedBlocks: true, 186 | removeTags: true, 187 | typeScriptUtilityTypes: true, 188 | tagsReplacement: null, 189 | }); 190 | expect(features.ExtendTypes).toHaveBeenCalledTimes(0); 191 | }); 192 | 193 | it('should be loaded without the modules on memberof feature', () => { 194 | // Given 195 | let sut = null; 196 | let features = null; 197 | // When 198 | ({ plugin: sut, features } = loadPlugin({ 199 | modulesOnMemberOf: false, 200 | })); 201 | // Then 202 | expect(sut.options).toEqual({ 203 | typedefImports: true, 204 | typeOfTypes: true, 205 | extendTypes: true, 206 | modulesOnMemberOf: false, 207 | modulesTypesShortName: true, 208 | parentTag: true, 209 | removeTaggedBlocks: true, 210 | removeTags: true, 211 | typeScriptUtilityTypes: true, 212 | tagsReplacement: null, 213 | }); 214 | expect(features.ModulesOnMemberOf).toHaveBeenCalledTimes(0); 215 | }); 216 | 217 | it('should be loaded without the modules types short name feature', () => { 218 | // Given 219 | let sut = null; 220 | let features = null; 221 | // When 222 | ({ plugin: sut, features } = loadPlugin({ 223 | modulesTypesShortName: false, 224 | })); 225 | // Then 226 | expect(sut.options).toEqual({ 227 | typedefImports: true, 228 | typeOfTypes: true, 229 | extendTypes: true, 230 | modulesOnMemberOf: true, 231 | modulesTypesShortName: false, 232 | parentTag: true, 233 | removeTaggedBlocks: true, 234 | removeTags: true, 235 | typeScriptUtilityTypes: true, 236 | tagsReplacement: null, 237 | }); 238 | expect(features.ModulesTypesShortName).toHaveBeenCalledTimes(0); 239 | }); 240 | 241 | it('should be loaded without the parent tag feature', () => { 242 | // Given 243 | let sut = null; 244 | let features = null; 245 | // When 246 | ({ plugin: sut, features } = loadPlugin({ 247 | parentTag: false, 248 | })); 249 | // Then 250 | expect(sut.options).toEqual({ 251 | typedefImports: true, 252 | typeOfTypes: true, 253 | extendTypes: true, 254 | modulesOnMemberOf: true, 255 | modulesTypesShortName: true, 256 | parentTag: false, 257 | removeTaggedBlocks: true, 258 | removeTags: true, 259 | typeScriptUtilityTypes: true, 260 | tagsReplacement: null, 261 | }); 262 | expect(features.TagsReplacement).toHaveBeenCalledTimes(0); 263 | }); 264 | 265 | it('should be loaded without the remove tagged blocks feature', () => { 266 | // Given 267 | let sut = null; 268 | let features = null; 269 | // When 270 | ({ plugin: sut, features } = loadPlugin({ 271 | removeTaggedBlocks: false, 272 | })); 273 | // Then 274 | expect(sut.options).toEqual({ 275 | typedefImports: true, 276 | typeOfTypes: true, 277 | extendTypes: true, 278 | modulesOnMemberOf: true, 279 | modulesTypesShortName: true, 280 | parentTag: true, 281 | removeTaggedBlocks: false, 282 | removeTags: true, 283 | typeScriptUtilityTypes: true, 284 | tagsReplacement: null, 285 | }); 286 | expect(features.RemoveTaggedBlocks).toHaveBeenCalledTimes(0); 287 | }); 288 | 289 | it('should be loaded without the remove tags feature', () => { 290 | // Given 291 | let sut = null; 292 | let features = null; 293 | // When 294 | ({ plugin: sut, features } = loadPlugin({ 295 | removeTags: false, 296 | })); 297 | // Then 298 | expect(sut.options).toEqual({ 299 | typedefImports: true, 300 | typeOfTypes: true, 301 | extendTypes: true, 302 | modulesOnMemberOf: true, 303 | modulesTypesShortName: true, 304 | parentTag: true, 305 | removeTaggedBlocks: true, 306 | removeTags: false, 307 | typeScriptUtilityTypes: true, 308 | tagsReplacement: null, 309 | }); 310 | expect(features.RemoveTags).toHaveBeenCalledTimes(0); 311 | }); 312 | 313 | it('should be loaded without the TS utility types feature', () => { 314 | // Given 315 | let sut = null; 316 | let features = null; 317 | // When 318 | ({ plugin: sut, features } = loadPlugin({ 319 | typeScriptUtilityTypes: false, 320 | })); 321 | // Then 322 | expect(sut.options).toEqual({ 323 | typedefImports: true, 324 | typeOfTypes: true, 325 | extendTypes: true, 326 | modulesOnMemberOf: true, 327 | modulesTypesShortName: true, 328 | parentTag: true, 329 | removeTaggedBlocks: true, 330 | removeTags: true, 331 | typeScriptUtilityTypes: false, 332 | tagsReplacement: null, 333 | }); 334 | expect(features.TSUtilitiesTypes).toHaveBeenCalledTimes(0); 335 | }); 336 | 337 | it('should be loaded the tags replacement feature configured', () => { 338 | // Given 339 | const dictionary = { 340 | parent: 'memberofof', 341 | }; 342 | let sut = null; 343 | let features = null; 344 | let events = null; 345 | // When 346 | ({ 347 | plugin: sut, 348 | features, 349 | events, 350 | } = loadPlugin({ 351 | tagsReplacement: dictionary, 352 | })); 353 | // Then 354 | expect(sut.handlers).toEqual({ 355 | parseBegin: expect.any(Function), 356 | beforeParse: expect.any(Function), 357 | }); 358 | expect(sut.options).toEqual({ 359 | typedefImports: true, 360 | typeOfTypes: true, 361 | extendTypes: true, 362 | modulesOnMemberOf: true, 363 | modulesTypesShortName: true, 364 | parentTag: true, 365 | removeTaggedBlocks: true, 366 | removeTags: true, 367 | typeScriptUtilityTypes: true, 368 | tagsReplacement: dictionary, 369 | }); 370 | expect(features.TagsReplacement).toHaveBeenCalledTimes(2); 371 | expect(features.TagsReplacement).toHaveBeenNthCalledWith( 372 | 1, 373 | { parent: 'memberof' }, 374 | events, 375 | EVENT_NAMES, 376 | ); 377 | expect(features.TagsReplacement).toHaveBeenNthCalledWith( 378 | 2, 379 | dictionary, 380 | events, 381 | EVENT_NAMES, 382 | ); 383 | }); 384 | 385 | it('should emit the parseBegin event using the local emitter', () => { 386 | // Given 387 | const event = { 388 | sourcefiles: [], 389 | }; 390 | let sut = null; 391 | let events = null; 392 | // When 393 | ({ plugin: sut, events } = loadPlugin()); 394 | sut.handlers.parseBegin(event); 395 | // Then 396 | expect(events.emit).toHaveBeenCalledTimes(1); 397 | expect(events.emit).toHaveBeenCalledWith(EVENT_NAMES.parseBegin, event); 398 | }); 399 | 400 | it('should reduce the contents of a file using the beforeParse event', () => { 401 | // Given 402 | const comment = [ 403 | '/**', 404 | ' * @typedef {import("family").Daughter} Daughter', 405 | ' * @typedef {Daughter} Rosario', 406 | ' * @typedef {Daughter} Pilar', 407 | ' */', 408 | ].join('\n'); 409 | const contents = "const hello = () => 'world';"; 410 | const source = `${comment}${contents}`; 411 | const filename = 'daughters.js'; 412 | const firstListenerResult = 1; 413 | const firstListener = jest.fn(() => firstListenerResult); 414 | const secondListenerResult = 2; 415 | const secondListener = jest.fn(() => secondListenerResult); 416 | const events = { 417 | emit: jest.fn(), 418 | listeners: jest.fn(() => [firstListener, secondListener]), 419 | }; 420 | const event = { 421 | source, 422 | filename, 423 | }; 424 | let sut = null; 425 | // When 426 | ({ plugin: sut } = loadPlugin(null, events)); 427 | sut.handlers.beforeParse(event); 428 | // Then 429 | expect(event.source).toEqual(secondListenerResult); 430 | expect(events.emit).toHaveBeenCalledTimes(1); 431 | expect(events.emit).toHaveBeenCalledWith(EVENT_NAMES.newComment, comment, filename); 432 | expect(events.listeners).toHaveBeenCalledTimes(1); 433 | expect(events.listeners).toHaveBeenCalledWith(EVENT_NAMES.commentsReady); 434 | expect(firstListener).toHaveBeenCalledTimes(1); 435 | expect(firstListener).toHaveBeenCalledWith(source, filename); 436 | expect(secondListener).toHaveBeenCalledTimes(1); 437 | expect(secondListener).toHaveBeenCalledWith(firstListenerResult, filename); 438 | }); 439 | 440 | it('should ignore comments with the @ignore tag', () => { 441 | // Given 442 | const comment = [ 443 | '/**', 444 | ' * @typedef {import("family").Daughter} Daughter', 445 | ' * @typedef {Daughter} Rosario', 446 | ' * @typedef {Daughter} Pilar', 447 | ' * @ignore', 448 | ' */', 449 | ].join('\n'); 450 | const contents = "const hello = () => 'world';"; 451 | const source = `${comment}${contents}`; 452 | const filename = 'daughters.js'; 453 | const listener = jest.fn(); 454 | const events = { 455 | emit: jest.fn(), 456 | listeners: jest.fn(() => [listener]), 457 | }; 458 | const event = { 459 | source, 460 | filename, 461 | }; 462 | let sut = null; 463 | // When 464 | ({ plugin: sut } = loadPlugin(null, events)); 465 | sut.handlers.beforeParse(event); 466 | // Then 467 | expect(events.emit).toHaveBeenCalledTimes(0); 468 | }); 469 | }); 470 | -------------------------------------------------------------------------------- /utils/scripts/docs: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | jsdoc -c .jsdoc.js 3 | -------------------------------------------------------------------------------- /utils/scripts/lint: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | lint-staged 3 | -------------------------------------------------------------------------------- /utils/scripts/lint-all: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | eslint ./ --ext .js 3 | -------------------------------------------------------------------------------- /utils/scripts/prepare: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | is-ci || husky install 3 | -------------------------------------------------------------------------------- /utils/scripts/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | jest -c ./.jestrc.js $@ 3 | -------------------------------------------------------------------------------- /utils/scripts/todo: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | leasot 'src/**/*.js' -x 3 | --------------------------------------------------------------------------------