├── docs.ts ├── docs ├── index.md ├── modules │ ├── index.md │ ├── bin.ts.md │ ├── index.ts.md │ ├── Production.ts.md │ ├── Spawn.ts.md │ ├── Config.ts.md │ ├── Core.ts.md │ ├── FileSystem.ts.md │ ├── Logger.ts.md │ ├── Parser.ts.md │ ├── Module.ts.md │ └── Markdown.ts.md └── _config.yml ├── .gitignore ├── tsconfig.build.json ├── .prettierrc ├── src ├── bin.ts ├── Production.ts ├── Spawn.ts ├── index.ts ├── Config.ts ├── FileSystem.ts ├── Logger.ts ├── Module.ts ├── Markdown.ts ├── Core.ts └── Parser.ts ├── .vscode └── settings.json ├── tsconfig.eslint.json ├── vitest.config.ts ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md └── workflows │ └── main.yml ├── tsconfig.json ├── test ├── util.ts ├── Config.ts ├── Logger.ts ├── Module.ts ├── Markdown.ts └── Parser.ts ├── LICENSE ├── .eslintrc.json ├── package.json ├── CHANGELOG.md └── README.md /docs.ts: -------------------------------------------------------------------------------- 1 | import './src/bin' 2 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Home 3 | nav_order: 1 4 | --- 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | node_modules 3 | lib 4 | dev 5 | coverage 6 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["./src"] 4 | } 5 | -------------------------------------------------------------------------------- /docs/modules/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Modules 3 | has_children: true 4 | permalink: /docs/modules 5 | nav_order: 2 6 | --- -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "printWidth": 120, 5 | "trailingComma": "none" 6 | } 7 | -------------------------------------------------------------------------------- /docs/modules/bin.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: bin.ts 3 | nav_order: 1 4 | parent: Modules 5 | --- 6 | 7 | ## bin overview 8 | 9 | CLI 10 | 11 | Added in v0.2.0 12 | 13 | --- 14 | 15 |

Table of contents

16 | 17 | --- 18 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | remote_theme: pmarsceill/just-the-docs 2 | 3 | # Enable or disable the site search 4 | search_enabled: true 5 | 6 | # Aux links for the upper right navigation 7 | aux_links: 8 | 'docs-ts on GitHub': 9 | - 'https://github.com/gcanti/docs-ts' 10 | -------------------------------------------------------------------------------- /src/bin.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * CLI 5 | * 6 | * @since 0.2.0 7 | */ 8 | 9 | import chalk from 'chalk' 10 | 11 | import { main } from '.' 12 | 13 | main().catch((e) => { 14 | console.log(chalk.bold.red('Unexpected Error')) 15 | console.error(e) 16 | process.exit(1) 17 | }) 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib", 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "eslint.validate": ["typescript"], 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll.eslint": true 7 | }, 8 | "files.insertFinalNewline": true 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "allowJs": true, 5 | "baseUrl": "." 6 | }, 7 | "include": [ 8 | "./src/**/*.ts", 9 | "./test/**/*.ts", 10 | "./dtslint/**/*.ts", 11 | "./examples/**/*.ts", 12 | "./benchmark/**/*.ts", 13 | "./vitest.config.ts", 14 | "./scripts/**/*.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from "vite" 3 | 4 | export default defineConfig({ 5 | test: { 6 | include: ["./test/**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"], 7 | exclude: ["./test/**/util.ts", "./test/fixtures/**/*.ts"], 8 | globals: true, 9 | coverage: { 10 | provider: "c8" 11 | } 12 | } 13 | }) 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Before submitting a pull request,** please make sure the following is done: 2 | 3 | - Fork [the repository](https://github.com/gcanti/docs-ts) and create your branch from `master`. 4 | - Run `npm install` in the repository root. 5 | - If you've fixed a bug or added code that should be tested, add tests! 6 | - Ensure the test suite passes (`npm test`). 7 | -------------------------------------------------------------------------------- /docs/modules/index.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: index.ts 3 | nav_order: 5 4 | parent: Modules 5 | --- 6 | 7 | ## index overview 8 | 9 | Added in v0.2.0 10 | 11 | --- 12 | 13 |

Table of contents

14 | 15 | - [main](#main) 16 | - [main](#main-1) 17 | 18 | --- 19 | 20 | # main 21 | 22 | ## main 23 | 24 | **Signature** 25 | 26 | ```ts 27 | export declare const main: Task.Task 28 | ``` 29 | 30 | Added in v0.6.0 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Do you want to request a _feature_ or report a _bug_?** 2 | 3 | **What is the current behavior?** 4 | 5 | **If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem via https://codesandbox.io/ or similar.** 6 | 7 | **What is the expected behavior?** 8 | 9 | **Which versions of docs-ts, and which browser and OS are affected by this issue? Did this work in previous versions of docs-ts?** 10 | -------------------------------------------------------------------------------- /src/Production.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 0.8.0 3 | */ 4 | import * as Core from './Core' 5 | import { FileSystem } from './FileSystem' 6 | import { Logger } from './Logger' 7 | import { spawn } from './Spawn' 8 | 9 | /** 10 | * @category production 11 | * @since 0.8.0 12 | */ 13 | export const capabilities: Core.Capabilities = { 14 | spawn: spawn, 15 | fileSystem: FileSystem, 16 | logger: Logger, 17 | addFile: (file) => (project) => project.addSourceFileAtPath(file.path) 18 | } 19 | -------------------------------------------------------------------------------- /docs/modules/Production.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Production.ts 3 | nav_order: 10 4 | parent: Modules 5 | --- 6 | 7 | ## Production overview 8 | 9 | Added in v0.8.0 10 | 11 | --- 12 | 13 |

Table of contents

14 | 15 | - [production](#production) 16 | - [capabilities](#capabilities) 17 | 18 | --- 19 | 20 | # production 21 | 22 | ## capabilities 23 | 24 | **Signature** 25 | 26 | ```ts 27 | export declare const capabilities: Core.Capabilities 28 | ``` 29 | 30 | Added in v0.8.0 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "lib", 4 | "target": "es5", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "lib": ["es6"], 8 | "sourceMap": false, 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitReturns": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "stripInternal": true, 17 | "skipLibCheck": true, 18 | "types": ["vitest/globals"] 19 | }, 20 | "include": ["./src", "./test"] 21 | } 22 | -------------------------------------------------------------------------------- /test/util.ts: -------------------------------------------------------------------------------- 1 | import * as E from 'fp-ts/Either' 2 | import { pipe } from 'fp-ts/lib/function' 3 | 4 | export const assertLeft = (either: E.Either, onLeft: (e: E) => void) => 5 | pipe( 6 | either, 7 | E.fold(onLeft, (right) => { 8 | console.log(right) 9 | 10 | throw new Error('Expected Left') 11 | }) 12 | ) 13 | 14 | export const assertRight = (either: E.Either, onRight: (e: A) => void) => 15 | pipe( 16 | either, 17 | E.fold((left) => { 18 | console.log(left) 19 | 20 | throw new Error('Expected Right') 21 | }, onRight) 22 | ) 23 | -------------------------------------------------------------------------------- /docs/modules/Spawn.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Spawn.ts 3 | nav_order: 11 4 | parent: Modules 5 | --- 6 | 7 | ## Spawn overview 8 | 9 | Added in v0.6.0 10 | 11 | --- 12 | 13 |

Table of contents

14 | 15 | - [utils](#utils) 16 | - [spawn](#spawn) 17 | 18 | --- 19 | 20 | # utils 21 | 22 | ## spawn 23 | 24 | Executes a command like: 25 | 26 | ```sh 27 | ts-node examples/index.ts 28 | ``` 29 | 30 | where `command = ts-node` and `executable = examples/index.ts` 31 | 32 | **Signature** 33 | 34 | ```ts 35 | export declare const spawn: (command: string, executable: string) => TE.TaskEither 36 | ``` 37 | 38 | Added in v0.6.0 39 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [16.17.1] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: npm install 27 | - run: npm run build --if-present 28 | - run: npm test 29 | -------------------------------------------------------------------------------- /src/Spawn.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 0.6.0 3 | */ 4 | import { spawnSync } from 'child_process' 5 | import * as E from 'fp-ts/Either' 6 | import { pipe } from 'fp-ts/function' 7 | import * as TE from 'fp-ts/TaskEither' 8 | 9 | /** 10 | * Executes a command like: 11 | * 12 | * ```sh 13 | * ts-node examples/index.ts 14 | * ``` 15 | * 16 | * where `command = ts-node` and `executable = examples/index.ts` 17 | * 18 | * @category utils 19 | * @since 0.6.0 20 | */ 21 | export const spawn = (command: string, executable: string): TE.TaskEither => 22 | pipe( 23 | TE.fromEither(E.tryCatch(() => spawnSync(command, [executable], { stdio: 'pipe', encoding: 'utf8' }), String)), 24 | TE.flatMap(({ status, stderr }) => (status === 0 ? TE.right(undefined) : TE.left(stderr))) 25 | ) 26 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 0.2.0 3 | */ 4 | import chalk from 'chalk' 5 | import * as Console from 'fp-ts/Console' 6 | import * as IO from 'fp-ts/IO' 7 | import * as Task from 'fp-ts/Task' 8 | import * as TaskEither from 'fp-ts/TaskEither' 9 | 10 | import * as Core from './Core' 11 | import { capabilities } from './Production' 12 | 13 | const exit = 14 | (code: 0 | 1): IO.IO => 15 | () => 16 | process.exit(code) 17 | 18 | const handleResult: (program: TaskEither.TaskEither) => Task.Task = TaskEither.matchE( 19 | (error) => Task.fromIO(IO.flatMap(Console.log(chalk.bold.red(error)), () => exit(1))), 20 | () => Task.fromIO(IO.flatMap(Console.log(chalk.bold.green('Docs generation succeeded!')), () => exit(0))) 21 | ) 22 | 23 | /** 24 | * @category main 25 | * @since 0.6.0 26 | */ 27 | export const main: Task.Task = handleResult(Core.main(capabilities)) 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Giulio Canti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/modules/Config.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Config.ts 3 | nav_order: 2 4 | parent: Modules 5 | --- 6 | 7 | ## Config overview 8 | 9 | Added in v0.6.0 10 | 11 | --- 12 | 13 |

Table of contents

14 | 15 | - [model](#model) 16 | - [Config (interface)](#config-interface) 17 | - [utils](#utils) 18 | - [decode](#decode) 19 | 20 | --- 21 | 22 | # model 23 | 24 | ## Config (interface) 25 | 26 | **Signature** 27 | 28 | ```ts 29 | export interface Config { 30 | readonly projectName: string 31 | readonly projectHomepage: string 32 | readonly srcDir: string 33 | readonly outDir: string 34 | readonly theme: string 35 | readonly enableSearch: boolean 36 | readonly enforceDescriptions: boolean 37 | readonly enforceExamples: boolean 38 | readonly enforceVersion: boolean 39 | readonly exclude: ReadonlyArray 40 | readonly parseCompilerOptions: Record 41 | readonly examplesCompilerOptions: Record 42 | } 43 | ``` 44 | 45 | Added in v0.6.4 46 | 47 | # utils 48 | 49 | ## decode 50 | 51 | **Signature** 52 | 53 | ```ts 54 | export declare const decode: (input: unknown) => E.Either> 55 | ``` 56 | 57 | Added in v0.6.4 58 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "project": "./tsconfig.eslint.json" 4 | }, 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/eslint-recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "plugins": ["deprecation", "import", "simple-import-sort"], 11 | "rules": { 12 | "@typescript-eslint/array-type": ["warn", { "default": "generic", "readonly": "generic" }], 13 | "@typescript-eslint/prefer-readonly": "warn", 14 | "@typescript-eslint/member-delimiter-style": 0, 15 | "@typescript-eslint/no-non-null-assertion": "off", 16 | "@typescript-eslint/ban-types": "off", 17 | "@typescript-eslint/no-explicit-any": "off", 18 | "@typescript-eslint/no-empty-interface": "off", 19 | "no-unused-vars": "off", 20 | "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], 21 | "prefer-rest-params": "off", 22 | "prefer-spread": "off", 23 | "deprecation/deprecation": "error", 24 | "import/first": "error", 25 | "import/no-cycle": "error", 26 | "import/newline-after-import": "error", 27 | "import/no-duplicates": "error", 28 | "import/no-unresolved": "off", 29 | "import/order": "off", 30 | "simple-import-sort/imports": "error" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 0.6.0 3 | */ 4 | import * as E from 'fp-ts/Either' 5 | import { pipe } from 'fp-ts/function' 6 | import * as D from 'io-ts/Decoder' 7 | 8 | /** 9 | * @category model 10 | * @since 0.6.4 11 | */ 12 | export interface Config { 13 | readonly projectName: string 14 | readonly projectHomepage: string 15 | readonly srcDir: string 16 | readonly outDir: string 17 | readonly theme: string 18 | readonly enableSearch: boolean 19 | readonly enforceDescriptions: boolean 20 | readonly enforceExamples: boolean 21 | readonly enforceVersion: boolean 22 | readonly exclude: ReadonlyArray 23 | readonly parseCompilerOptions: Record 24 | readonly examplesCompilerOptions: Record 25 | } 26 | 27 | const ConfigDecoder = D.partial({ 28 | projectName: D.string, 29 | projectHomepage: D.string, 30 | srcDir: D.string, 31 | outDir: D.string, 32 | theme: D.string, 33 | enableSearch: D.boolean, 34 | enforceDescriptions: D.boolean, 35 | enforceExamples: D.boolean, 36 | enforceVersion: D.boolean, 37 | exclude: D.array(D.string), 38 | parseCompilerOptions: D.UnknownRecord, 39 | examplesCompilerOptions: D.UnknownRecord 40 | }) 41 | 42 | /** 43 | * @since 0.6.4 44 | */ 45 | export const decode = (input: unknown): E.Either> => 46 | pipe(ConfigDecoder.decode(input), E.mapLeft(D.draw)) 47 | -------------------------------------------------------------------------------- /test/Config.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import * as E from 'fp-ts/Either' 3 | import * as RA from 'fp-ts/ReadonlyArray' 4 | 5 | import * as _ from '../src/Config' 6 | 7 | describe.concurrent('Config', () => { 8 | describe.concurrent('decode', () => { 9 | it('should decode a valid configuration object', async () => { 10 | const config: unknown = { 11 | srcDir: 'src', 12 | outDir: 'docs', 13 | theme: 'pmarsceill/just-the-docs', 14 | enableSearch: true, 15 | enforceDescriptions: false, 16 | enforceExamples: false, 17 | exclude: RA.empty 18 | } 19 | 20 | assert.deepStrictEqual(_.decode(config), E.right(config)) 21 | }) 22 | 23 | it('should decode a valid partial configuration object', async () => { 24 | const config: unknown = { 25 | exclude: RA.of('subdirectory/**/*.ts') 26 | } 27 | 28 | assert.deepStrictEqual(_.decode({}), E.right({})) 29 | assert.deepStrictEqual(_.decode(config), E.right(config)) 30 | }) 31 | 32 | it('should not decode a configuration object with invalid keys', async () => { 33 | const config: unknown = { 34 | srcDir: 'src', 35 | outDir: 'docs', 36 | theme: 1, 37 | enableSearch: true, 38 | enforceDescriptions: false, 39 | enforceExamples: false, 40 | exclude: RA.empty 41 | } 42 | 43 | assert.deepStrictEqual( 44 | _.decode(config), 45 | E.left(`optional property "theme" 46 | └─ cannot decode 1, should be string`) 47 | ) 48 | }) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /docs/modules/Core.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Core.ts 3 | nav_order: 3 4 | parent: Modules 5 | --- 6 | 7 | ## Core overview 8 | 9 | Added in v0.6.0 10 | 11 | --- 12 | 13 |

Table of contents

14 | 15 | - [main](#main) 16 | - [main](#main-1) 17 | - [model](#model) 18 | - [Capabilities (interface)](#capabilities-interface) 19 | - [EnvironmentWithConfig (interface)](#environmentwithconfig-interface) 20 | - [Program (interface)](#program-interface) 21 | - [ProgramWithConfig (interface)](#programwithconfig-interface) 22 | 23 | --- 24 | 25 | # main 26 | 27 | ## main 28 | 29 | **Signature** 30 | 31 | ```ts 32 | export declare const main: Program 33 | ``` 34 | 35 | Added in v0.6.0 36 | 37 | # model 38 | 39 | ## Capabilities (interface) 40 | 41 | **Signature** 42 | 43 | ````ts 44 | export interface Capabilities { 45 | /** 46 | * Executes a command like: 47 | * 48 | * ```sh 49 | * ts-node examples/index.ts 50 | * ``` 51 | * 52 | * where `command = ts-node` and `executable = examples/index.ts` 53 | */ 54 | readonly spawn: (command: string, executable: string) => TaskEither.TaskEither 55 | readonly fileSystem: FileSystem 56 | readonly logger: Logger 57 | readonly addFile: (file: File) => (project: ast.Project) => void 58 | } 59 | ```` 60 | 61 | Added in v0.6.0 62 | 63 | ## EnvironmentWithConfig (interface) 64 | 65 | **Signature** 66 | 67 | ```ts 68 | export interface EnvironmentWithConfig extends Capabilities { 69 | readonly config: Config.Config 70 | } 71 | ``` 72 | 73 | Added in v0.6.0 74 | 75 | ## Program (interface) 76 | 77 | **Signature** 78 | 79 | ```ts 80 | export interface Program extends RTE.ReaderTaskEither {} 81 | ``` 82 | 83 | Added in v0.6.0 84 | 85 | ## ProgramWithConfig (interface) 86 | 87 | **Signature** 88 | 89 | ```ts 90 | export interface ProgramWithConfig extends RTE.ReaderTaskEither {} 91 | ``` 92 | 93 | Added in v0.6.0 94 | -------------------------------------------------------------------------------- /docs/modules/FileSystem.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: FileSystem.ts 3 | nav_order: 4 4 | parent: Modules 5 | --- 6 | 7 | ## FileSystem overview 8 | 9 | Added in v0.6.0 10 | 11 | --- 12 | 13 |

Table of contents

14 | 15 | - [constructors](#constructors) 16 | - [File](#file) 17 | - [instances](#instances) 18 | - [FileSystem](#filesystem) 19 | - [model](#model) 20 | - [File (interface)](#file-interface) 21 | - [FileSystem (interface)](#filesystem-interface) 22 | 23 | --- 24 | 25 | # constructors 26 | 27 | ## File 28 | 29 | By default files are readonly (`overwrite = false`). 30 | 31 | **Signature** 32 | 33 | ```ts 34 | export declare const File: (path: string, content: string, overwrite?: boolean) => File 35 | ``` 36 | 37 | Added in v0.6.0 38 | 39 | # instances 40 | 41 | ## FileSystem 42 | 43 | **Signature** 44 | 45 | ```ts 46 | export declare const FileSystem: FileSystem 47 | ``` 48 | 49 | Added in v0.6.0 50 | 51 | # model 52 | 53 | ## File (interface) 54 | 55 | Represents a file which can be optionally overwriteable. 56 | 57 | **Signature** 58 | 59 | ```ts 60 | export interface File { 61 | readonly path: string 62 | readonly content: string 63 | readonly overwrite: boolean 64 | } 65 | ``` 66 | 67 | Added in v0.6.0 68 | 69 | ## FileSystem (interface) 70 | 71 | Represents operations that can be performed on a file system. 72 | 73 | **Signature** 74 | 75 | ```ts 76 | export interface FileSystem { 77 | readonly readFile: (path: string) => TE.TaskEither 78 | /** 79 | * If the parent directory does not exist, it's created. 80 | */ 81 | readonly writeFile: (path: string, content: string) => TE.TaskEither 82 | readonly exists: (path: string) => TE.TaskEither 83 | /** 84 | * Removes a file or directory based upon the specified pattern. The directory can have contents. 85 | * If the path does not exist, silently does nothing. 86 | */ 87 | readonly remove: (pattern: string) => TE.TaskEither 88 | /** 89 | * Searches for files matching the specified glob pattern. 90 | */ 91 | readonly search: (pattern: string, exclude: ReadonlyArray) => TE.TaskEither> 92 | } 93 | ``` 94 | 95 | Added in v0.6.0 96 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs-ts", 3 | "version": "0.8.0", 4 | "description": "Documentation tool for TypeScript packages", 5 | "files": [ 6 | "lib" 7 | ], 8 | "main": "lib/index.js", 9 | "bin": "lib/bin.js", 10 | "typings": "lib/index.d.ts", 11 | "scripts": { 12 | "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"", 13 | "lint-fix": "eslint --fix \"src/**/*.ts\" \"test/**/*.ts\"", 14 | "vitest": "vitest", 15 | "coverage": "vitest run --coverage", 16 | "prettier": "prettier --parser typescript --list-different \"{src,test}/**/*.ts\"", 17 | "fix-prettier": "prettier --parser typescript --write \"{src,test}/**/*.ts\"", 18 | "test": "npm run prettier && npm run vitest && npm run docs", 19 | "clean": "rimraf rm -rf lib/*", 20 | "build": "npm run clean && tsc -p tsconfig.build.json", 21 | "prepublish": "npm run build", 22 | "mocha": "mocha -r ts-node/register test/*.ts", 23 | "doctoc": "doctoc README.md --title \"**Table of contents**\"", 24 | "docs": "ts-node docs.ts" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/gcanti/docs-ts.git" 29 | }, 30 | "author": "Giulio Canti ", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/gcanti/docs-ts/issues" 34 | }, 35 | "homepage": "https://github.com/gcanti/docs-ts", 36 | "dependencies": { 37 | "chalk": "^2.4.2", 38 | "doctrine": "^3.0.0", 39 | "fp-ts": "^2.15.0", 40 | "fs-extra": "^7.0.1", 41 | "glob": "^7.1.6", 42 | "io-ts": "^2.2.20", 43 | "logging-ts": "^0.3.4", 44 | "markdown-toc": "^1.2.0", 45 | "rimraf": "^2.7.1", 46 | "ts-morph": "^9.1.0", 47 | "ts-node": "^8.10.2" 48 | }, 49 | "devDependencies": { 50 | "@types/doctrine": "0.0.3", 51 | "@types/fs-extra": "^5.0.5", 52 | "@types/glob": "^7.1.3", 53 | "@types/node": "^18.16.6", 54 | "@types/prettier": "^1.19.1", 55 | "@types/rimraf": "^2.0.4", 56 | "@typescript-eslint/eslint-plugin": "^5.59.5", 57 | "@typescript-eslint/parser": "^5.59.5", 58 | "@vitest/coverage-c8": "^0.31.0", 59 | "doctoc": "^1.4.0", 60 | "eslint": "^8.40.0", 61 | "eslint-plugin-deprecation": "^1.4.1", 62 | "eslint-plugin-import": "^2.27.5", 63 | "eslint-plugin-simple-import-sort": "^10.0.0", 64 | "prettier": "^2.2.1", 65 | "typescript": "^5.0.4", 66 | "vite": "^4.3.5", 67 | "vitest": "^0.31.0" 68 | }, 69 | "peerDependencies": { 70 | "prettier": "^2.0.0", 71 | "typescript": "^5.x" 72 | }, 73 | "tags": [], 74 | "keywords": [] 75 | } 76 | -------------------------------------------------------------------------------- /docs/modules/Logger.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Logger.ts 3 | nav_order: 6 4 | parent: Modules 5 | --- 6 | 7 | ## Logger overview 8 | 9 | Added in v0.6.0 10 | 11 | --- 12 | 13 |

Table of contents

14 | 15 | - [constructors](#constructors) 16 | - [LogEntry](#logentry) 17 | - [instances](#instances) 18 | - [Logger](#logger) 19 | - [showEntry](#showentry) 20 | - [model](#model) 21 | - [LogEntry (interface)](#logentry-interface) 22 | - [LogLevel (type alias)](#loglevel-type-alias) 23 | - [Logger (interface)](#logger-interface) 24 | - [utils](#utils) 25 | - [debug](#debug) 26 | - [error](#error) 27 | - [info](#info) 28 | 29 | --- 30 | 31 | # constructors 32 | 33 | ## LogEntry 34 | 35 | **Signature** 36 | 37 | ```ts 38 | export declare const LogEntry: (message: string, date: Date, level: LogLevel) => LogEntry 39 | ``` 40 | 41 | Added in v0.6.0 42 | 43 | # instances 44 | 45 | ## Logger 46 | 47 | **Signature** 48 | 49 | ```ts 50 | export declare const Logger: Logger 51 | ``` 52 | 53 | Added in v0.6.0 54 | 55 | ## showEntry 56 | 57 | **Signature** 58 | 59 | ```ts 60 | export declare const showEntry: S.Show 61 | ``` 62 | 63 | Added in v0.6.0 64 | 65 | # model 66 | 67 | ## LogEntry (interface) 68 | 69 | **Signature** 70 | 71 | ```ts 72 | export interface LogEntry { 73 | readonly message: string 74 | readonly date: Date 75 | readonly level: LogLevel 76 | } 77 | ``` 78 | 79 | Added in v0.6.0 80 | 81 | ## LogLevel (type alias) 82 | 83 | **Signature** 84 | 85 | ```ts 86 | export type LogLevel = 'DEBUG' | 'ERROR' | 'INFO' 87 | ``` 88 | 89 | Added in v0.6.0 90 | 91 | ## Logger (interface) 92 | 93 | **Signature** 94 | 95 | ```ts 96 | export interface Logger { 97 | readonly debug: (message: string) => TE.TaskEither 98 | readonly error: (message: string) => TE.TaskEither 99 | readonly info: (message: string) => TE.TaskEither 100 | } 101 | ``` 102 | 103 | Added in v0.6.0 104 | 105 | # utils 106 | 107 | ## debug 108 | 109 | **Signature** 110 | 111 | ```ts 112 | export declare const debug: (message: string) => T.Task 113 | ``` 114 | 115 | Added in v0.6.0 116 | 117 | ## error 118 | 119 | **Signature** 120 | 121 | ```ts 122 | export declare const error: (message: string) => T.Task 123 | ``` 124 | 125 | Added in v0.6.0 126 | 127 | ## info 128 | 129 | **Signature** 130 | 131 | ```ts 132 | export declare const info: (message: string) => T.Task 133 | ``` 134 | 135 | Added in v0.6.0 136 | -------------------------------------------------------------------------------- /docs/modules/Parser.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Parser.ts 3 | nav_order: 9 4 | parent: Modules 5 | --- 6 | 7 | ## Parser overview 8 | 9 | Added in v0.6.0 10 | 11 | --- 12 | 13 |

Table of contents

14 | 15 | - [model](#model) 16 | - [Parser (interface)](#parser-interface) 17 | - [ParserEnv (interface)](#parserenv-interface) 18 | - [parsers](#parsers) 19 | - [parseClasses](#parseclasses) 20 | - [parseConstants](#parseconstants) 21 | - [parseExports](#parseexports) 22 | - [parseFiles](#parsefiles) 23 | - [parseFunctions](#parsefunctions) 24 | - [parseInterfaces](#parseinterfaces) 25 | - [parseModule](#parsemodule) 26 | - [parseTypeAliases](#parsetypealiases) 27 | 28 | --- 29 | 30 | # model 31 | 32 | ## Parser (interface) 33 | 34 | **Signature** 35 | 36 | ```ts 37 | export interface Parser
extends RE.ReaderEither {} 38 | ``` 39 | 40 | Added in v0.6.0 41 | 42 | ## ParserEnv (interface) 43 | 44 | **Signature** 45 | 46 | ```ts 47 | export interface ParserEnv extends EnvironmentWithConfig { 48 | readonly path: RNEA.ReadonlyNonEmptyArray 49 | readonly sourceFile: ast.SourceFile 50 | } 51 | ``` 52 | 53 | Added in v0.6.0 54 | 55 | # parsers 56 | 57 | ## parseClasses 58 | 59 | **Signature** 60 | 61 | ```ts 62 | export declare const parseClasses: Parser 63 | ``` 64 | 65 | Added in v0.6.0 66 | 67 | ## parseConstants 68 | 69 | **Signature** 70 | 71 | ```ts 72 | export declare const parseConstants: Parser 73 | ``` 74 | 75 | Added in v0.6.0 76 | 77 | ## parseExports 78 | 79 | **Signature** 80 | 81 | ```ts 82 | export declare const parseExports: Parser 83 | ``` 84 | 85 | Added in v0.6.0 86 | 87 | ## parseFiles 88 | 89 | **Signature** 90 | 91 | ```ts 92 | export declare const parseFiles: ( 93 | files: ReadonlyArray 94 | ) => RTE.ReaderTaskEither> 95 | ``` 96 | 97 | Added in v0.6.0 98 | 99 | ## parseFunctions 100 | 101 | **Signature** 102 | 103 | ```ts 104 | export declare const parseFunctions: Parser 105 | ``` 106 | 107 | Added in v0.6.0 108 | 109 | ## parseInterfaces 110 | 111 | **Signature** 112 | 113 | ```ts 114 | export declare const parseInterfaces: Parser 115 | ``` 116 | 117 | Added in v0.6.0 118 | 119 | ## parseModule 120 | 121 | **Signature** 122 | 123 | ```ts 124 | export declare const parseModule: Parser 125 | ``` 126 | 127 | Added in v0.6.0 128 | 129 | ## parseTypeAliases 130 | 131 | **Signature** 132 | 133 | ```ts 134 | export declare const parseTypeAliases: Parser 135 | ``` 136 | 137 | Added in v0.6.0 138 | -------------------------------------------------------------------------------- /src/FileSystem.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 0.6.0 3 | */ 4 | import { flow, pipe } from 'fp-ts/function' 5 | import * as TE from 'fp-ts/TaskEither' 6 | import * as fs from 'fs-extra' 7 | import * as glob from 'glob' 8 | import * as rimraf from 'rimraf' 9 | 10 | import { toErrorMsg } from './Logger' 11 | 12 | /** 13 | * Represents operations that can be performed on a file system. 14 | * 15 | * @category model 16 | * @since 0.6.0 17 | */ 18 | export interface FileSystem { 19 | readonly readFile: (path: string) => TE.TaskEither 20 | /** 21 | * If the parent directory does not exist, it's created. 22 | */ 23 | readonly writeFile: (path: string, content: string) => TE.TaskEither 24 | readonly exists: (path: string) => TE.TaskEither 25 | /** 26 | * Removes a file or directory based upon the specified pattern. The directory can have contents. 27 | * If the path does not exist, silently does nothing. 28 | */ 29 | readonly remove: (pattern: string) => TE.TaskEither 30 | /** 31 | * Searches for files matching the specified glob pattern. 32 | */ 33 | readonly search: (pattern: string, exclude: ReadonlyArray) => TE.TaskEither> 34 | } 35 | 36 | /** 37 | * Represents a file which can be optionally overwriteable. 38 | * 39 | * @category model 40 | * @since 0.6.0 41 | */ 42 | export interface File { 43 | readonly path: string 44 | readonly content: string 45 | readonly overwrite: boolean 46 | } 47 | 48 | /** 49 | * By default files are readonly (`overwrite = false`). 50 | * 51 | * @category constructors 52 | * @since 0.6.0 53 | */ 54 | export const File = (path: string, content: string, overwrite = false): File => ({ 55 | path, 56 | content, 57 | overwrite 58 | }) 59 | 60 | const readFile: (path: string, encoding: string) => TE.TaskEither = TE.taskify< 61 | string, 62 | string, 63 | Error, 64 | string 65 | >(fs.readFile) 66 | 67 | const writeFile: ( 68 | path: string, 69 | data: string, 70 | options: { 71 | readonly encoding?: string 72 | readonly flag?: string 73 | readonly mode?: number 74 | } 75 | ) => TE.TaskEither = TE.taskify(fs.outputFile) 76 | 77 | const exists: (path: string) => TE.TaskEither = TE.taskify(fs.pathExists) 78 | 79 | const remove: (path: string, options: rimraf.Options) => TE.TaskEither = TE.taskify< 80 | string, 81 | rimraf.Options, 82 | Error, 83 | void 84 | >(rimraf) 85 | 86 | const search: (pattern: string, options: glob.IOptions) => TE.TaskEither> = TE.taskify< 87 | string, 88 | glob.IOptions, 89 | Error, 90 | ReadonlyArray 91 | >(glob) 92 | 93 | /** 94 | * @category instances 95 | * @since 0.6.0 96 | */ 97 | export const FileSystem: FileSystem = { 98 | readFile: (path) => pipe(readFile(path, 'utf8'), TE.mapLeft(toErrorMsg)), 99 | writeFile: (path, content) => pipe(writeFile(path, content, { encoding: 'utf8' }), TE.mapLeft(toErrorMsg)), 100 | exists: flow(exists, TE.mapLeft(toErrorMsg)), 101 | remove: (pattern) => pipe(remove(pattern, {}), TE.mapLeft(toErrorMsg)), 102 | search: (pattern, exclude) => pipe(search(pattern, { ignore: exclude }), TE.mapLeft(toErrorMsg)) 103 | } 104 | -------------------------------------------------------------------------------- /test/Logger.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import chalk from 'chalk' 3 | 4 | import * as _ from '../src/Logger' 5 | 6 | describe.concurrent('Logger', () => { 7 | describe.concurrent('constructors', () => { 8 | it('LogEntry', () => { 9 | const date = new Date(Date.now()) 10 | 11 | assert.deepStrictEqual(_.LogEntry('test', date, 'DEBUG'), { 12 | message: 'test', 13 | date, 14 | level: 'DEBUG' 15 | }) 16 | }) 17 | }) 18 | 19 | describe('utils', () => { 20 | it('debug', async () => { 21 | const log_ = console.log 22 | 23 | const logs: Array = [] 24 | 25 | console.log = (a: any) => { 26 | logs.push(a) 27 | } 28 | 29 | const date = new Date(Date.now()) 30 | 31 | await _.debug('test')() 32 | 33 | assert.deepStrictEqual(logs, [ 34 | chalk.cyan(`${date.toLocaleDateString()} | ${date.toLocaleTimeString()} | DEBUG | test`) 35 | ]) 36 | 37 | console.log = log_ 38 | }) 39 | 40 | it('error', async () => { 41 | const log_ = console.log 42 | 43 | const logs: Array = [] 44 | 45 | console.log = (a: any) => { 46 | logs.push(a) 47 | } 48 | 49 | const date = new Date(Date.now()) 50 | 51 | await _.error('test')() 52 | 53 | assert.deepStrictEqual(logs, [ 54 | chalk.bold.red(`${date.toLocaleDateString()} | ${date.toLocaleTimeString()} | ERROR | test`) 55 | ]) 56 | 57 | console.log = log_ 58 | }) 59 | 60 | it('info', async () => { 61 | const log_ = console.log 62 | 63 | const logs: Array = [] 64 | 65 | console.log = (a: any) => { 66 | logs.push(a) 67 | } 68 | 69 | const date = new Date(Date.now()) 70 | 71 | await _.info('test')() 72 | 73 | assert.deepStrictEqual(logs, [ 74 | chalk.bold.magenta(`${date.toLocaleDateString()} | ${date.toLocaleTimeString()} | INFO | test`) 75 | ]) 76 | 77 | console.log = log_ 78 | }) 79 | 80 | it('toErrorMsg', () => { 81 | const msg = _.toErrorMsg(new Error('test')) 82 | 83 | assert.strictEqual(msg, 'test') 84 | }) 85 | }) 86 | 87 | describe.concurrent('instances', () => { 88 | it('showEntry', () => { 89 | const date = new Date(Date.now()) 90 | const entry = _.LogEntry('test', date, 'DEBUG') 91 | 92 | assert.deepStrictEqual( 93 | _.showEntry.show(entry), 94 | `${date.toLocaleDateString()} | ${date.toLocaleTimeString()} | DEBUG | test` 95 | ) 96 | }) 97 | 98 | it('Logger', async () => { 99 | const log_ = console.log 100 | 101 | const logs: Array = [] 102 | 103 | console.log = (a: any) => { 104 | logs.push(a) 105 | } 106 | 107 | const date = new Date(Date.now()) 108 | 109 | await _.Logger.debug('test')() 110 | await _.Logger.error('test')() 111 | await _.Logger.info('test')() 112 | 113 | assert.deepStrictEqual(logs, [ 114 | chalk.cyan(`${date.toLocaleDateString()} | ${date.toLocaleTimeString()} | DEBUG | test`), 115 | chalk.bold.red(`${date.toLocaleDateString()} | ${date.toLocaleTimeString()} | ERROR | test`), 116 | chalk.bold.magenta(`${date.toLocaleDateString()} | ${date.toLocaleTimeString()} | INFO | test`) 117 | ]) 118 | 119 | console.log = log_ 120 | }) 121 | }) 122 | }) 123 | -------------------------------------------------------------------------------- /src/Logger.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 0.6.0 3 | */ 4 | import chalk from 'chalk' 5 | import * as C from 'fp-ts/Console' 6 | import * as D from 'fp-ts/Date' 7 | import { pipe } from 'fp-ts/function' 8 | import * as M from 'fp-ts/Monoid' 9 | import * as S from 'fp-ts/Show' 10 | import * as T from 'fp-ts/Task' 11 | import * as TE from 'fp-ts/TaskEither' 12 | import * as L from 'logging-ts/lib/Task' 13 | 14 | // ------------------------------------------------------------------------------------- 15 | // model 16 | // ------------------------------------------------------------------------------------- 17 | 18 | /** 19 | * @category model 20 | * @since 0.6.0 21 | */ 22 | export interface Logger { 23 | readonly debug: (message: string) => TE.TaskEither 24 | readonly error: (message: string) => TE.TaskEither 25 | readonly info: (message: string) => TE.TaskEither 26 | } 27 | 28 | /** 29 | * @category model 30 | * @since 0.6.0 31 | */ 32 | export type LogLevel = 'DEBUG' | 'ERROR' | 'INFO' 33 | 34 | /** 35 | * @category model 36 | * @since 0.6.0 37 | */ 38 | export interface LogEntry { 39 | readonly message: string 40 | readonly date: Date 41 | readonly level: LogLevel 42 | } 43 | 44 | // ------------------------------------------------------------------------------------- 45 | // constructors 46 | // ------------------------------------------------------------------------------------- 47 | 48 | /** 49 | * @category constructors 50 | * @since 0.6.0 51 | */ 52 | export const LogEntry = (message: string, date: Date, level: LogLevel): LogEntry => ({ 53 | message, 54 | date, 55 | level 56 | }) 57 | 58 | // ------------------------------------------------------------------------------------- 59 | // utils 60 | // ------------------------------------------------------------------------------------- 61 | 62 | const getLoggerEntry = 63 | (withColor: (...message: ReadonlyArray) => string): L.LoggerTask => 64 | (entry) => 65 | T.fromIO(C.log(withColor(showEntry.show(entry)))) 66 | 67 | const debugLogger = L.filter(getLoggerEntry(chalk.cyan), (e) => e.level === 'DEBUG') 68 | 69 | const errorLogger = L.filter(getLoggerEntry(chalk.bold.red), (e) => e.level === 'ERROR') 70 | 71 | const infoLogger = L.filter(getLoggerEntry(chalk.bold.magenta), (e) => e.level === 'INFO') 72 | 73 | const mainLogger = pipe([debugLogger, errorLogger, infoLogger], M.concatAll(L.getMonoid())) 74 | 75 | const logWithLevel = 76 | (level: LogLevel) => 77 | (message: string): T.Task => 78 | pipe( 79 | T.fromIO(D.create), 80 | T.flatMap((date) => mainLogger({ message, date, level })) 81 | ) 82 | 83 | /** 84 | * @category utils 85 | * @since 0.6.0 86 | */ 87 | export const debug: (message: string) => T.Task = logWithLevel('DEBUG') 88 | 89 | /** 90 | * @category utils 91 | * @since 0.6.0 92 | */ 93 | export const error: (message: string) => T.Task = logWithLevel('ERROR') 94 | 95 | /** 96 | * @category utils 97 | * @since 0.6.0 98 | */ 99 | export const info: (message: string) => T.Task = logWithLevel('INFO') 100 | 101 | // ------------------------------------------------------------------------------------- 102 | // instances 103 | // ------------------------------------------------------------------------------------- 104 | 105 | const showDate: S.Show = { 106 | show: (date) => `${date.toLocaleDateString()} | ${date.toLocaleTimeString()}` 107 | } 108 | 109 | /** 110 | * @category instances 111 | * @since 0.6.0 112 | */ 113 | export const showEntry: S.Show = { 114 | show: ({ message, date, level }) => `${showDate.show(date)} | ${level} | ${message}` 115 | } 116 | 117 | /** 118 | * @internal 119 | */ 120 | export const toErrorMsg = (err: Error): string => String(err.message) 121 | 122 | /** 123 | * @category instances 124 | * @since 0.6.0 125 | */ 126 | export const Logger: Logger = { 127 | debug: (message) => pipe(TE.fromTask(debug(message)), TE.mapLeft(toErrorMsg)), 128 | error: (message) => pipe(TE.fromTask(error(message)), TE.mapLeft(toErrorMsg)), 129 | info: (message) => pipe(TE.fromTask(info(message)), TE.mapLeft(toErrorMsg)) 130 | } 131 | -------------------------------------------------------------------------------- /test/Module.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import { pipe } from 'fp-ts/function' 3 | import * as O from 'fp-ts/Option' 4 | import * as RA from 'fp-ts/ReadonlyArray' 5 | 6 | import * as _ from '../src/Module' 7 | 8 | const documentable = (name: string) => _.Documentable(name, O.none, O.some('1.0.0'), false, RA.empty, O.none) 9 | 10 | describe.concurrent('Module', () => { 11 | describe.concurrent('constructors', () => { 12 | it('Documentable', () => { 13 | assert.deepStrictEqual(documentable('A'), { 14 | name: 'A', 15 | description: O.none, 16 | since: O.some('1.0.0'), 17 | deprecated: false, 18 | examples: RA.empty, 19 | category: O.none 20 | }) 21 | }) 22 | 23 | it('Module', () => { 24 | const m = _.Module( 25 | documentable('test'), 26 | ['src', 'index.ts'], 27 | RA.empty, 28 | RA.empty, 29 | RA.empty, 30 | RA.empty, 31 | RA.empty, 32 | RA.empty 33 | ) 34 | 35 | assert.deepStrictEqual(m, { 36 | ...documentable('test'), 37 | path: ['src', 'index.ts'], 38 | classes: RA.empty, 39 | interfaces: RA.empty, 40 | functions: RA.empty, 41 | typeAliases: RA.empty, 42 | constants: RA.empty, 43 | exports: RA.empty 44 | }) 45 | }) 46 | 47 | it('Class', () => { 48 | const c = _.Class(documentable('A'), 'declare class A { constructor() }', RA.empty, RA.empty, RA.empty) 49 | 50 | assert.deepStrictEqual(c, { 51 | _tag: 'Class', 52 | ...documentable('A'), 53 | signature: 'declare class A { constructor() }', 54 | methods: RA.empty, 55 | staticMethods: RA.empty, 56 | properties: RA.empty 57 | }) 58 | }) 59 | 60 | it('Constant', () => { 61 | const c = _.Constant(documentable('foo'), 'declare const foo: string') 62 | 63 | assert.deepStrictEqual(c, { 64 | _tag: 'Constant', 65 | ...documentable('foo'), 66 | signature: 'declare const foo: string' 67 | }) 68 | }) 69 | 70 | it('Method', () => { 71 | const m = _.Method(documentable('foo'), ['foo(): string']) 72 | 73 | assert.deepStrictEqual(m, { 74 | ...documentable('foo'), 75 | signatures: ['foo(): string'] 76 | }) 77 | }) 78 | 79 | it('Property', () => { 80 | const p = _.Property(documentable('foo'), 'foo: string') 81 | 82 | assert.deepStrictEqual(p, { 83 | ...documentable('foo'), 84 | signature: 'foo: string' 85 | }) 86 | }) 87 | 88 | it('Interface', () => { 89 | const i = _.Interface(documentable('A'), 'interface A {}') 90 | 91 | assert.deepStrictEqual(i, { 92 | _tag: 'Interface', 93 | ...documentable('A'), 94 | signature: 'interface A {}' 95 | }) 96 | }) 97 | 98 | it('Function', () => { 99 | const f = _.Function(documentable('func'), ['declare function func(): string']) 100 | 101 | assert.deepStrictEqual(f, { 102 | _tag: 'Function', 103 | ...documentable('func'), 104 | signatures: ['declare function func(): string'] 105 | }) 106 | }) 107 | 108 | it('TypeAlias', () => { 109 | const ta = _.TypeAlias(documentable('A'), 'type A = string') 110 | 111 | assert.deepStrictEqual(ta, { 112 | _tag: 'TypeAlias', 113 | ...documentable('A'), 114 | signature: 'type A = string' 115 | }) 116 | }) 117 | 118 | it('Export', () => { 119 | const e = _.Export(documentable('foo'), 'export declare const foo: string') 120 | 121 | assert.deepStrictEqual(e, { 122 | _tag: 'Export', 123 | ...documentable('foo'), 124 | signature: 'export declare const foo: string' 125 | }) 126 | }) 127 | }) 128 | 129 | describe.concurrent('instances', () => { 130 | it('ordModule', () => { 131 | const m1 = _.Module( 132 | documentable('test1'), 133 | ['src', 'test1.ts'], 134 | RA.empty, 135 | RA.empty, 136 | RA.empty, 137 | RA.empty, 138 | RA.empty, 139 | RA.empty 140 | ) 141 | 142 | const m2 = _.Module( 143 | documentable('test1'), 144 | ['src', 'test1.ts'], 145 | RA.empty, 146 | RA.empty, 147 | RA.empty, 148 | RA.empty, 149 | RA.empty, 150 | RA.empty 151 | ) 152 | 153 | const sorted = pipe([m2, m1], RA.sort(_.ordModule)) 154 | 155 | assert.deepStrictEqual(sorted, [m1, m2]) 156 | }) 157 | }) 158 | }) 159 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | > **Tags:** 4 | > 5 | > - [New Feature] 6 | > - [Bug Fix] 7 | > - [Breaking Change] 8 | > - [Documentation] 9 | > - [Internal] 10 | > - [Polish] 11 | > - [Experimental] 12 | 13 | **Note**: Gaps between patch versions are faulty/broken releases. 14 | **Note**: A feature tagged as Experimental is in a high state of flux, you're at risk of it changing without notice. 15 | 16 | # 0.8.0 17 | 18 | - update `fp-ts` version 19 | - simplify `Config` module 20 | - remove `Settings` 21 | - add `tsconfig.json` to `examples` folder when type checking examples 22 | - split `compilerOptions` in config to: 23 | - `parseCompilerOptions` 24 | - `examplesCompilerOptions` 25 | 26 | # 0.7.2 27 | 28 | remove extensions from `examples/index.ts` 29 | 30 | # 0.7.1 31 | 32 | make `docs-ts` compatible with typescript `^5.x` 33 | 34 | # 0.7.0 35 | 36 | - **New Feature** 37 | - add `compilerOptions` to configuration (@gcanti) 38 | 39 | # 0.6.10 40 | 41 | - **Bug Fix** 42 | - fix `Parser` ignoring `@internal`/`@ignore`d methods on classes (@IMax153) 43 | 44 | # 0.6.9 45 | 46 | - **Polish** 47 | - print error as non coerced string, #28 (@waynevanson) 48 | 49 | # 0.6.8 50 | 51 | - **Polish** 52 | - modules no longer require examples when `enforceExamples` is true (@IMax153) 53 | 54 | # 0.6.7 55 | 56 | - **Bug Fix** 57 | - use `strict: true` in project configuration, fix #36 (@gcanti) 58 | 59 | # 0.6.6 60 | 61 | - **Bug Fix** 62 | - simplify `@example` import replacement, #35 (@thought2) 63 | 64 | # 0.6.5 65 | 66 | - **Polish** 67 | - allow double quotes in `@example` project imports, #31 (@thought2) 68 | 69 | # 0.6.4 70 | 71 | - **New Feature** 72 | - add `projectHomepage` configuration property, closes #26 (@IMax153) 73 | 74 | # 0.6.3 75 | 76 | - **Polish** 77 | - fix modules not respecting config settings #24 (@IMax153) 78 | - move `prettier` to `peerDependencies`, closes #22 (@gcanti) 79 | 80 | # 0.6.2 81 | 82 | - **Breaking Change** 83 | 84 | - refactor `Markdown` module (@IMax153) 85 | - add `Markdown` constructors (@IMax153) 86 | - add tagged union of `Printable` types (@IMax153) 87 | - add `fold` destructor for `Markdown` (@IMax153) 88 | - add `Semigroup`, `Monoid`, and `Show` instances for `Markdown` (@IMax153) 89 | - add `printModule` helper function (@IMax153) 90 | - update `Parser` module (@IMax153) 91 | - add `ParserEnv` which extends `Environment` (@IMax153) 92 | - add `Ast` interface (@IMax153) 93 | - update `Core` module (@IMax153) 94 | - add `Program` and `Environment` types (@IMax153) 95 | - update `Capabilities` interface (@IMax153) 96 | - remove `Eff`, `MonadFileSystem`, and `MonadLog` types (@IMax153) 97 | - remove `MonadFileSystem` and `MonadLog` instances (@IMax153) 98 | - rename `domain` module to `Module` (@IMax153) 99 | - rename all constructors to match their respective types (@IMax153) 100 | 101 | - **New Feature** 102 | - add `Config` module (@IMax153) 103 | - support configuration through `docs-ts.json` file (@IMax153) 104 | - add `Config`, `ConfigBuilder` and `Settings` types (@IMax153) 105 | - add `build` constructor `ConfigBuilder` (@IMax153) 106 | - add `resolveSettings` destructor for creating `Settings` from a `ConfigBuilder` (@IMax153) 107 | - add combinators for manipulating a `ConfigBuilder` (@IMax153) 108 | - add `FileSystem` module (@IMax153) 109 | - add `FileSystem` instance (@IMax153) 110 | - add `File` constructor (@IMax153) 111 | - add `exists`, `readFile`, `remove`, `search`, and `writeFile` helper functions (@IMax153) 112 | - add `Logger` module (@IMax153) 113 | - add `LogEntry`, `LogLevel`, and `Logger` types (@IMax153) 114 | - add `showEntry` and `Logger` instances (@IMax153) 115 | - add `debug`, `error`, and `info` helper functions (@IMax153) 116 | - Add `Example` module (@IMax153) 117 | - add `run` helper function (@IMax153) 118 | 119 | # 0.5.3 120 | 121 | - **Polish** 122 | - add support for TypeScript `4.x`, closes #19 (@gcanti) 123 | 124 | # 0.5.2 125 | 126 | - **Polish** 127 | - use ts-node.cmd on windows, #15 (@mattiamanzati) 128 | 129 | # 0.5.1 130 | 131 | - **Bug Fix** 132 | - should not return ignore function declarations (@gcanti) 133 | - should not return internal function declarations (@gcanti) 134 | - should output the class name when there's an error in a property (@gcanti) 135 | 136 | # 0.5.0 137 | 138 | - **Breaking Change** 139 | - total refactoring (@gcanti) 140 | 141 | # 0.4.0 142 | 143 | - **Breaking Change** 144 | - the signature snippets are not valid TS (@gcanti) 145 | - add support for class properties (@gcanti) 146 | 147 | # 0.3.5 148 | 149 | - **Polish** 150 | - support any path in `src` in the examples, #12 (@gillchristian) 151 | 152 | # 0.3.4 153 | 154 | - **Polish** 155 | - remove `code` from headers (@gcanti) 156 | 157 | # 0.3.3 158 | 159 | - **Polish** 160 | - remove useless postfix (@gcanti) 161 | 162 | # 0.3.1 163 | 164 | - **Bug Fix** 165 | - add support for default type parameters (@gcanti) 166 | 167 | # 0.3.0 168 | 169 | - **Breaking Change** 170 | - modules now can/must be documented as usual (@gcanti) 171 | - required `@since` tag 172 | - no more `@file` tags (descriptione can be specified as usual) 173 | 174 | # 0.2.1 175 | 176 | - **Internal** 177 | - run `npm audit fix` (@gcanti) 178 | 179 | # 0.2.0 180 | 181 | - **Breaking Change** 182 | - replace `ts-simple-ast` with `ts-morph` (@gcanti) 183 | - make `@since` tag mandatory (@gcanti) 184 | - **New Feature** 185 | - add support for `ExportDeclaration`s (@gcanti) 186 | 187 | # 0.1.0 188 | 189 | upgrade to `fp-ts@2.0.0-rc.7` (@gcanti) 190 | 191 | - **Bug Fix** 192 | - fix static methods heading (@gcanti) 193 | 194 | # 0.0.3 195 | 196 | upgrade to `fp-ts@1.18.x` (@gcanti) 197 | 198 | # 0.0.2 199 | 200 | - **Bug Fix** 201 | - fix Windows Path Handling (@rzeigler) 202 | 203 | # 0.0.1 204 | 205 | Initial release 206 | -------------------------------------------------------------------------------- /src/Module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 0.6.0 3 | */ 4 | import { pipe } from 'fp-ts/function' 5 | import * as O from 'fp-ts/Option' 6 | import * as Ord from 'fp-ts/Ord' 7 | import * as S from 'fp-ts/string' 8 | 9 | // ------------------------------------------------------------------------------------- 10 | // model 11 | // ------------------------------------------------------------------------------------- 12 | 13 | /** 14 | * @category model 15 | * @since 0.6.0 16 | */ 17 | export interface Module extends Documentable { 18 | readonly path: ReadonlyArray 19 | readonly classes: ReadonlyArray 20 | readonly interfaces: ReadonlyArray 21 | readonly functions: ReadonlyArray 22 | readonly typeAliases: ReadonlyArray 23 | readonly constants: ReadonlyArray 24 | readonly exports: ReadonlyArray 25 | } 26 | 27 | /** 28 | * @category model 29 | * @since 0.6.0 30 | */ 31 | export interface Documentable { 32 | readonly name: string 33 | readonly description: O.Option 34 | readonly since: O.Option 35 | readonly deprecated: boolean 36 | readonly examples: ReadonlyArray 37 | readonly category: O.Option 38 | } 39 | 40 | /** 41 | * @category model 42 | * @since 0.6.0 43 | */ 44 | export interface Class extends Documentable { 45 | readonly _tag: 'Class' 46 | readonly signature: string 47 | readonly methods: ReadonlyArray 48 | readonly staticMethods: ReadonlyArray 49 | readonly properties: ReadonlyArray 50 | } 51 | 52 | /** 53 | * @category model 54 | * @since 0.6.0 55 | */ 56 | export interface Method extends Documentable { 57 | readonly signatures: ReadonlyArray 58 | } 59 | 60 | /** 61 | * @category model 62 | * @since 0.6.0 63 | */ 64 | export interface Property extends Documentable { 65 | readonly signature: string 66 | } 67 | 68 | /** 69 | * @category model 70 | * @since 0.6.0 71 | */ 72 | export interface Interface extends Documentable { 73 | readonly _tag: 'Interface' 74 | readonly signature: string 75 | } 76 | 77 | /** 78 | * @category model 79 | * @since 0.6.0 80 | */ 81 | export interface Function extends Documentable { 82 | readonly _tag: 'Function' 83 | readonly signatures: ReadonlyArray 84 | } 85 | 86 | /** 87 | * @category model 88 | * @since 0.6.0 89 | */ 90 | export interface TypeAlias extends Documentable { 91 | readonly _tag: 'TypeAlias' 92 | readonly signature: string 93 | } 94 | 95 | /** 96 | * @category model 97 | * @since 0.6.0 98 | */ 99 | export interface Constant extends Documentable { 100 | readonly _tag: 'Constant' 101 | readonly signature: string 102 | } 103 | 104 | /** 105 | * @category model 106 | * @since 0.6.0 107 | */ 108 | export interface Export extends Documentable { 109 | readonly _tag: 'Export' 110 | readonly signature: string 111 | } 112 | 113 | /** 114 | * @category model 115 | * @since 0.6.0 116 | */ 117 | export type Example = string 118 | 119 | // ------------------------------------------------------------------------------------- 120 | // constructors 121 | // ------------------------------------------------------------------------------------- 122 | 123 | /** 124 | * @category constructors 125 | * @since 0.6.0 126 | */ 127 | export const Documentable = ( 128 | name: string, 129 | description: O.Option, 130 | since: O.Option, 131 | deprecated: boolean, 132 | examples: ReadonlyArray, 133 | category: O.Option 134 | ): Documentable => ({ name, description, since, deprecated, examples, category }) 135 | 136 | /** 137 | * @category constructors 138 | * @since 0.6.0 139 | */ 140 | export const Module = ( 141 | documentable: Documentable, 142 | path: ReadonlyArray, 143 | classes: ReadonlyArray, 144 | interfaces: ReadonlyArray, 145 | functions: ReadonlyArray, 146 | typeAliases: ReadonlyArray, 147 | constants: ReadonlyArray, 148 | exports: ReadonlyArray 149 | ): Module => ({ 150 | ...documentable, 151 | path, 152 | classes, 153 | interfaces, 154 | functions, 155 | typeAliases, 156 | constants, 157 | exports 158 | }) 159 | 160 | /** 161 | * @category constructors 162 | * @since 0.6.0 163 | */ 164 | export const Class = ( 165 | documentable: Documentable, 166 | signature: string, 167 | methods: ReadonlyArray, 168 | staticMethods: ReadonlyArray, 169 | properties: ReadonlyArray 170 | ): Class => ({ 171 | _tag: 'Class', 172 | ...documentable, 173 | signature, 174 | methods, 175 | staticMethods, 176 | properties 177 | }) 178 | 179 | /** 180 | * @category constructors 181 | * @since 0.6.0 182 | */ 183 | export const Constant = (documentable: Documentable, signature: string): Constant => ({ 184 | _tag: 'Constant', 185 | ...documentable, 186 | signature 187 | }) 188 | 189 | /** 190 | * @category constructors 191 | * @since 0.6.0 192 | */ 193 | export const Method = (documentable: Documentable, signatures: ReadonlyArray): Method => ({ 194 | ...documentable, 195 | signatures 196 | }) 197 | 198 | /** 199 | * @category constructors 200 | * @since 0.6.0 201 | */ 202 | export const Property = (documentable: Documentable, signature: string): Property => ({ 203 | ...documentable, 204 | signature 205 | }) 206 | 207 | /** 208 | * @category constructors 209 | * @since 0.6.0 210 | */ 211 | export const Interface = (documentable: Documentable, signature: string): Interface => ({ 212 | _tag: 'Interface', 213 | ...documentable, 214 | signature 215 | }) 216 | 217 | /** 218 | * @category constructors 219 | * @since 0.6.0 220 | */ 221 | export const Function = (documentable: Documentable, signatures: ReadonlyArray): Function => ({ 222 | _tag: 'Function', 223 | ...documentable, 224 | signatures 225 | }) 226 | 227 | /** 228 | * @category constructors 229 | * @since 0.6.0 230 | */ 231 | export const TypeAlias = (documentable: Documentable, signature: string): TypeAlias => ({ 232 | _tag: 'TypeAlias', 233 | ...documentable, 234 | signature 235 | }) 236 | 237 | /** 238 | * @category constructors 239 | * @since 0.6.0 240 | */ 241 | export const Export = (documentable: Documentable, signature: string): Export => ({ 242 | _tag: 'Export', 243 | ...documentable, 244 | signature 245 | }) 246 | 247 | // ------------------------------------------------------------------------------------- 248 | // instances 249 | // ------------------------------------------------------------------------------------- 250 | 251 | /** 252 | * @category instances 253 | * @since 0.6.0 254 | */ 255 | export const ordModule: Ord.Ord = pipe( 256 | S.Ord, 257 | Ord.contramap((module: Module) => module.path.join('/').toLowerCase()) 258 | ) 259 | -------------------------------------------------------------------------------- /docs/modules/Module.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Module.ts 3 | nav_order: 8 4 | parent: Modules 5 | --- 6 | 7 | ## Module overview 8 | 9 | Added in v0.6.0 10 | 11 | --- 12 | 13 |

Table of contents

14 | 15 | - [constructors](#constructors) 16 | - [Class](#class) 17 | - [Constant](#constant) 18 | - [Documentable](#documentable) 19 | - [Export](#export) 20 | - [Function](#function) 21 | - [Interface](#interface) 22 | - [Method](#method) 23 | - [Module](#module) 24 | - [Property](#property) 25 | - [TypeAlias](#typealias) 26 | - [instances](#instances) 27 | - [ordModule](#ordmodule) 28 | - [model](#model) 29 | - [Class (interface)](#class-interface) 30 | - [Constant (interface)](#constant-interface) 31 | - [Documentable (interface)](#documentable-interface) 32 | - [Example (type alias)](#example-type-alias) 33 | - [Export (interface)](#export-interface) 34 | - [Function (interface)](#function-interface) 35 | - [Interface (interface)](#interface-interface) 36 | - [Method (interface)](#method-interface) 37 | - [Module (interface)](#module-interface) 38 | - [Property (interface)](#property-interface) 39 | - [TypeAlias (interface)](#typealias-interface) 40 | 41 | --- 42 | 43 | # constructors 44 | 45 | ## Class 46 | 47 | **Signature** 48 | 49 | ```ts 50 | export declare const Class: ( 51 | documentable: Documentable, 52 | signature: string, 53 | methods: ReadonlyArray, 54 | staticMethods: ReadonlyArray, 55 | properties: ReadonlyArray 56 | ) => Class 57 | ``` 58 | 59 | Added in v0.6.0 60 | 61 | ## Constant 62 | 63 | **Signature** 64 | 65 | ```ts 66 | export declare const Constant: (documentable: Documentable, signature: string) => Constant 67 | ``` 68 | 69 | Added in v0.6.0 70 | 71 | ## Documentable 72 | 73 | **Signature** 74 | 75 | ```ts 76 | export declare const Documentable: ( 77 | name: string, 78 | description: O.Option, 79 | since: O.Option, 80 | deprecated: boolean, 81 | examples: ReadonlyArray, 82 | category: O.Option 83 | ) => Documentable 84 | ``` 85 | 86 | Added in v0.6.0 87 | 88 | ## Export 89 | 90 | **Signature** 91 | 92 | ```ts 93 | export declare const Export: (documentable: Documentable, signature: string) => Export 94 | ``` 95 | 96 | Added in v0.6.0 97 | 98 | ## Function 99 | 100 | **Signature** 101 | 102 | ```ts 103 | export declare const Function: (documentable: Documentable, signatures: ReadonlyArray) => Function 104 | ``` 105 | 106 | Added in v0.6.0 107 | 108 | ## Interface 109 | 110 | **Signature** 111 | 112 | ```ts 113 | export declare const Interface: (documentable: Documentable, signature: string) => Interface 114 | ``` 115 | 116 | Added in v0.6.0 117 | 118 | ## Method 119 | 120 | **Signature** 121 | 122 | ```ts 123 | export declare const Method: (documentable: Documentable, signatures: ReadonlyArray) => Method 124 | ``` 125 | 126 | Added in v0.6.0 127 | 128 | ## Module 129 | 130 | **Signature** 131 | 132 | ```ts 133 | export declare const Module: ( 134 | documentable: Documentable, 135 | path: ReadonlyArray, 136 | classes: ReadonlyArray, 137 | interfaces: ReadonlyArray, 138 | functions: ReadonlyArray, 139 | typeAliases: ReadonlyArray, 140 | constants: ReadonlyArray, 141 | exports: ReadonlyArray 142 | ) => Module 143 | ``` 144 | 145 | Added in v0.6.0 146 | 147 | ## Property 148 | 149 | **Signature** 150 | 151 | ```ts 152 | export declare const Property: (documentable: Documentable, signature: string) => Property 153 | ``` 154 | 155 | Added in v0.6.0 156 | 157 | ## TypeAlias 158 | 159 | **Signature** 160 | 161 | ```ts 162 | export declare const TypeAlias: (documentable: Documentable, signature: string) => TypeAlias 163 | ``` 164 | 165 | Added in v0.6.0 166 | 167 | # instances 168 | 169 | ## ordModule 170 | 171 | **Signature** 172 | 173 | ```ts 174 | export declare const ordModule: Ord.Ord 175 | ``` 176 | 177 | Added in v0.6.0 178 | 179 | # model 180 | 181 | ## Class (interface) 182 | 183 | **Signature** 184 | 185 | ```ts 186 | export interface Class extends Documentable { 187 | readonly _tag: 'Class' 188 | readonly signature: string 189 | readonly methods: ReadonlyArray 190 | readonly staticMethods: ReadonlyArray 191 | readonly properties: ReadonlyArray 192 | } 193 | ``` 194 | 195 | Added in v0.6.0 196 | 197 | ## Constant (interface) 198 | 199 | **Signature** 200 | 201 | ```ts 202 | export interface Constant extends Documentable { 203 | readonly _tag: 'Constant' 204 | readonly signature: string 205 | } 206 | ``` 207 | 208 | Added in v0.6.0 209 | 210 | ## Documentable (interface) 211 | 212 | **Signature** 213 | 214 | ```ts 215 | export interface Documentable { 216 | readonly name: string 217 | readonly description: O.Option 218 | readonly since: O.Option 219 | readonly deprecated: boolean 220 | readonly examples: ReadonlyArray 221 | readonly category: O.Option 222 | } 223 | ``` 224 | 225 | Added in v0.6.0 226 | 227 | ## Example (type alias) 228 | 229 | **Signature** 230 | 231 | ```ts 232 | export type Example = string 233 | ``` 234 | 235 | Added in v0.6.0 236 | 237 | ## Export (interface) 238 | 239 | **Signature** 240 | 241 | ```ts 242 | export interface Export extends Documentable { 243 | readonly _tag: 'Export' 244 | readonly signature: string 245 | } 246 | ``` 247 | 248 | Added in v0.6.0 249 | 250 | ## Function (interface) 251 | 252 | **Signature** 253 | 254 | ```ts 255 | export interface Function extends Documentable { 256 | readonly _tag: 'Function' 257 | readonly signatures: ReadonlyArray 258 | } 259 | ``` 260 | 261 | Added in v0.6.0 262 | 263 | ## Interface (interface) 264 | 265 | **Signature** 266 | 267 | ```ts 268 | export interface Interface extends Documentable { 269 | readonly _tag: 'Interface' 270 | readonly signature: string 271 | } 272 | ``` 273 | 274 | Added in v0.6.0 275 | 276 | ## Method (interface) 277 | 278 | **Signature** 279 | 280 | ```ts 281 | export interface Method extends Documentable { 282 | readonly signatures: ReadonlyArray 283 | } 284 | ``` 285 | 286 | Added in v0.6.0 287 | 288 | ## Module (interface) 289 | 290 | **Signature** 291 | 292 | ```ts 293 | export interface Module extends Documentable { 294 | readonly path: ReadonlyArray 295 | readonly classes: ReadonlyArray 296 | readonly interfaces: ReadonlyArray 297 | readonly functions: ReadonlyArray 298 | readonly typeAliases: ReadonlyArray 299 | readonly constants: ReadonlyArray 300 | readonly exports: ReadonlyArray 301 | } 302 | ``` 303 | 304 | Added in v0.6.0 305 | 306 | ## Property (interface) 307 | 308 | **Signature** 309 | 310 | ```ts 311 | export interface Property extends Documentable { 312 | readonly signature: string 313 | } 314 | ``` 315 | 316 | Added in v0.6.0 317 | 318 | ## TypeAlias (interface) 319 | 320 | **Signature** 321 | 322 | ```ts 323 | export interface TypeAlias extends Documentable { 324 | readonly _tag: 'TypeAlias' 325 | readonly signature: string 326 | } 327 | ``` 328 | 329 | Added in v0.6.0 330 | -------------------------------------------------------------------------------- /docs/modules/Markdown.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown.ts 3 | nav_order: 7 4 | parent: Modules 5 | --- 6 | 7 | ## Markdown overview 8 | 9 | Added in v0.6.0 10 | 11 | --- 12 | 13 |

Table of contents

14 | 15 | - [constructors](#constructors) 16 | - [Bold](#bold) 17 | - [Fence](#fence) 18 | - [Header](#header) 19 | - [Newline](#newline) 20 | - [Paragraph](#paragraph) 21 | - [PlainText](#plaintext) 22 | - [PlainTexts](#plaintexts) 23 | - [Strikethrough](#strikethrough) 24 | - [destructors](#destructors) 25 | - [fold](#fold) 26 | - [instances](#instances) 27 | - [monoidMarkdown](#monoidmarkdown) 28 | - [semigroupMarkdown](#semigroupmarkdown) 29 | - [showMarkdown](#showmarkdown) 30 | - [model](#model) 31 | - [Bold (interface)](#bold-interface) 32 | - [Fence (interface)](#fence-interface) 33 | - [Header (interface)](#header-interface) 34 | - [Markdown (type alias)](#markdown-type-alias) 35 | - [Newline (interface)](#newline-interface) 36 | - [Paragraph (interface)](#paragraph-interface) 37 | - [PlainText (interface)](#plaintext-interface) 38 | - [PlainTexts (interface)](#plaintexts-interface) 39 | - [Printable (type alias)](#printable-type-alias) 40 | - [Strikethrough (interface)](#strikethrough-interface) 41 | - [printers](#printers) 42 | - [printClass](#printclass) 43 | - [printConstant](#printconstant) 44 | - [printExport](#printexport) 45 | - [printFunction](#printfunction) 46 | - [printInterface](#printinterface) 47 | - [printModule](#printmodule) 48 | - [printTypeAlias](#printtypealias) 49 | 50 | --- 51 | 52 | # constructors 53 | 54 | ## Bold 55 | 56 | **Signature** 57 | 58 | ```ts 59 | export declare const Bold: (content: Markdown) => Markdown 60 | ``` 61 | 62 | Added in v0.6.0 63 | 64 | ## Fence 65 | 66 | **Signature** 67 | 68 | ```ts 69 | export declare const Fence: (language: string, content: Markdown) => Markdown 70 | ``` 71 | 72 | Added in v0.6.0 73 | 74 | ## Header 75 | 76 | **Signature** 77 | 78 | ```ts 79 | export declare const Header: (level: number, content: Markdown) => Markdown 80 | ``` 81 | 82 | Added in v0.6.0 83 | 84 | ## Newline 85 | 86 | **Signature** 87 | 88 | ```ts 89 | export declare const Newline: Markdown 90 | ``` 91 | 92 | Added in v0.6.0 93 | 94 | ## Paragraph 95 | 96 | **Signature** 97 | 98 | ```ts 99 | export declare const Paragraph: (content: Markdown) => Markdown 100 | ``` 101 | 102 | Added in v0.6.0 103 | 104 | ## PlainText 105 | 106 | **Signature** 107 | 108 | ```ts 109 | export declare const PlainText: (content: string) => Markdown 110 | ``` 111 | 112 | Added in v0.6.0 113 | 114 | ## PlainTexts 115 | 116 | **Signature** 117 | 118 | ```ts 119 | export declare const PlainTexts: (content: ReadonlyArray) => Markdown 120 | ``` 121 | 122 | Added in v0.6.0 123 | 124 | ## Strikethrough 125 | 126 | **Signature** 127 | 128 | ```ts 129 | export declare const Strikethrough: (content: Markdown) => Markdown 130 | ``` 131 | 132 | Added in v0.6.0 133 | 134 | # destructors 135 | 136 | ## fold 137 | 138 | **Signature** 139 | 140 | ```ts 141 | export declare const fold: (patterns: { 142 | readonly Bold: (content: Markdown) => R 143 | readonly Fence: (language: string, content: Markdown) => R 144 | readonly Header: (level: number, content: Markdown) => R 145 | readonly Newline: () => R 146 | readonly Paragraph: (content: Markdown) => R 147 | readonly PlainText: (content: string) => R 148 | readonly PlainTexts: (content: ReadonlyArray) => R 149 | readonly Strikethrough: (content: Markdown) => R 150 | }) => (markdown: Markdown) => R 151 | ``` 152 | 153 | Added in v0.6.0 154 | 155 | # instances 156 | 157 | ## monoidMarkdown 158 | 159 | **Signature** 160 | 161 | ```ts 162 | export declare const monoidMarkdown: M.Monoid 163 | ``` 164 | 165 | Added in v0.6.0 166 | 167 | ## semigroupMarkdown 168 | 169 | **Signature** 170 | 171 | ```ts 172 | export declare const semigroupMarkdown: Semigroup 173 | ``` 174 | 175 | Added in v0.6.0 176 | 177 | ## showMarkdown 178 | 179 | **Signature** 180 | 181 | ```ts 182 | export declare const showMarkdown: Show 183 | ``` 184 | 185 | Added in v0.6.0 186 | 187 | # model 188 | 189 | ## Bold (interface) 190 | 191 | **Signature** 192 | 193 | ```ts 194 | export interface Bold { 195 | readonly _tag: 'Bold' 196 | readonly content: Markdown 197 | } 198 | ``` 199 | 200 | Added in v0.6.0 201 | 202 | ## Fence (interface) 203 | 204 | **Signature** 205 | 206 | ```ts 207 | export interface Fence { 208 | readonly _tag: 'Fence' 209 | readonly language: string 210 | readonly content: Markdown 211 | } 212 | ``` 213 | 214 | Added in v0.6.0 215 | 216 | ## Header (interface) 217 | 218 | **Signature** 219 | 220 | ```ts 221 | export interface Header { 222 | readonly _tag: 'Header' 223 | readonly level: number 224 | readonly content: Markdown 225 | } 226 | ``` 227 | 228 | Added in v0.6.0 229 | 230 | ## Markdown (type alias) 231 | 232 | **Signature** 233 | 234 | ```ts 235 | export type Markdown = Bold | Fence | Header | Newline | Paragraph | PlainText | PlainTexts | Strikethrough 236 | ``` 237 | 238 | Added in v0.6.0 239 | 240 | ## Newline (interface) 241 | 242 | **Signature** 243 | 244 | ```ts 245 | export interface Newline { 246 | readonly _tag: 'Newline' 247 | } 248 | ``` 249 | 250 | Added in v0.6.0 251 | 252 | ## Paragraph (interface) 253 | 254 | **Signature** 255 | 256 | ```ts 257 | export interface Paragraph { 258 | readonly _tag: 'Paragraph' 259 | readonly content: Markdown 260 | } 261 | ``` 262 | 263 | Added in v0.6.0 264 | 265 | ## PlainText (interface) 266 | 267 | **Signature** 268 | 269 | ```ts 270 | export interface PlainText { 271 | readonly _tag: 'PlainText' 272 | readonly content: string 273 | } 274 | ``` 275 | 276 | Added in v0.6.0 277 | 278 | ## PlainTexts (interface) 279 | 280 | **Signature** 281 | 282 | ```ts 283 | export interface PlainTexts { 284 | readonly _tag: 'PlainTexts' 285 | readonly content: ReadonlyArray 286 | } 287 | ``` 288 | 289 | Added in v0.6.0 290 | 291 | ## Printable (type alias) 292 | 293 | **Signature** 294 | 295 | ```ts 296 | export type Printable = Class | Constant | Export | Function | Interface | TypeAlias 297 | ``` 298 | 299 | Added in v0.6.0 300 | 301 | ## Strikethrough (interface) 302 | 303 | **Signature** 304 | 305 | ```ts 306 | export interface Strikethrough { 307 | readonly _tag: 'Strikethrough' 308 | readonly content: Markdown 309 | } 310 | ``` 311 | 312 | Added in v0.6.0 313 | 314 | # printers 315 | 316 | ## printClass 317 | 318 | **Signature** 319 | 320 | ```ts 321 | export declare const printClass: (c: Class) => string 322 | ``` 323 | 324 | Added in v0.6.0 325 | 326 | ## printConstant 327 | 328 | **Signature** 329 | 330 | ```ts 331 | export declare const printConstant: (c: Constant) => string 332 | ``` 333 | 334 | Added in v0.6.0 335 | 336 | ## printExport 337 | 338 | **Signature** 339 | 340 | ```ts 341 | export declare const printExport: (e: Export) => string 342 | ``` 343 | 344 | Added in v0.6.0 345 | 346 | ## printFunction 347 | 348 | **Signature** 349 | 350 | ```ts 351 | export declare const printFunction: (f: Function) => string 352 | ``` 353 | 354 | Added in v0.6.0 355 | 356 | ## printInterface 357 | 358 | **Signature** 359 | 360 | ```ts 361 | export declare const printInterface: (i: Interface) => string 362 | ``` 363 | 364 | Added in v0.6.0 365 | 366 | ## printModule 367 | 368 | **Signature** 369 | 370 | ```ts 371 | export declare const printModule: (module: Module, order: number) => string 372 | ``` 373 | 374 | Added in v0.6.0 375 | 376 | ## printTypeAlias 377 | 378 | **Signature** 379 | 380 | ```ts 381 | export declare const printTypeAlias: (f: TypeAlias) => string 382 | ``` 383 | 384 | Added in v0.6.0 385 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | **Table of contents** 5 | 6 | - [Installation:](#installation) 7 | - [Via `npm`](#via-npm) 8 | - [Via `yarn`](#via-yarn) 9 | - [Via `npx` (on-demand)](#via-npx-on-demand) 10 | - [Why](#why) 11 | - [Usage](#usage) 12 | - [Supported JSDoc Tags](#supported-jsdoc-tags) 13 | - [Example](#example) 14 | - [Configuration](#configuration) 15 | - [Documentation](#documentation) 16 | - [FAQ](#faq) 17 | - [License](#license) 18 | 19 | 20 | 21 | > A simple, opinionated, zero-configuration tool for creating beautiful documentation for TypeScript projects. 22 | 23 | **:warning: DISCLAIMER :warning:** 24 | `docs-ts` is used primarily as an **opinionated** documentation tool for libraries in the `fp-ts` ecosystem. The structure of source code documentation expected by `docs-ts` can be best understood by reviewing the source code of the [`fp-ts`](https://github.com/gcanti/fp-ts) repository. 25 | 26 | ## Installation: 27 | 28 | #### Via `npm` 29 | 30 | ``` 31 | npm install -D docs-ts 32 | ``` 33 | 34 | #### Via `yarn` 35 | 36 | ``` 37 | yarn add -D docs-ts 38 | ``` 39 | 40 | #### Via `npx` (on-demand) 41 | 42 | ``` 43 | npx docs-ts 44 | ``` 45 | 46 | ## Why 47 | 48 | Creating and maintaing documentation for a TypeScript project of any size can quickly become a herculean task. `docs-ts` simplifies this process by allowing you to co-locate your documentation with its associated code. You simply annotate your code with JSDoc comments, and then the CLI will generate beautiful markdown documents containing all of the documentation and examples you associated with your code. In addition, the generated output of `docs-ts` can be used as a [publishing source](https://docs.github.com/en/free-pro-team@latest/github/working-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site#choosing-a-publishing-source) for your repository's documentation on GitHub. 49 | 50 | ## Usage 51 | 52 | Using `docs-ts` is as simple as annotating your code with JSDoc comments. Specialized JSDoc tags can be used to perform various functions, such as grouping associated code together, versioning documentation, and running and testing source code. A full list of supported JSDoc tags can be found below. 53 | 54 | #### Supported JSDoc Tags 55 | 56 | | Tag | Description | Default | 57 | | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | 58 | | `@category` | Groups associated module exports together in the generated documentation. | `'utils'` | 59 | | `@example` | Allows usage examples to be provided for your source code. All examples are type checked using `ts-node`. Examples are also run using `ts-node` and the NodeJS [assert](https://nodejs.org/api/assert.html) module can be used for on-the-fly testing (see [example](#example) below). | | 60 | | `@since` | Allows for documenting most recent library version in which a given piece of source code was updated. | | 61 | | `@deprecated` | Marks source code as deprecated, which will ~~strikethrough~~ the name of the annotated module or function in the generated documentation. | `false` | 62 | | `@internal` | Prevents `docs-ts` from generating documentation for the annotated block of code. Additionally, if the `stripInternal` flag is set to `true` in `tsconfig.json`, TypeScript will not emit declarations for the annotated code. | | 63 | | `@ignore` | Prevents `docs-ts` from generating documentation for the annotated block of code. | | 64 | 65 | By default, `docs-ts` will search for files in the `src` directory and will output generated files into a `docs` directory. For information on how to configure `docs-ts`, see the [Configuration](#configuration) section below. 66 | 67 | ### Example 68 | 69 | The best usage examples of `docs-ts` can be found in [`fp-ts` ecosystem](https://gcanti.github.io/fp-ts/ecosystem/) libraries that generate their documentation with `docs-ts`, such as the main [`fp-ts`](https://github.com/gcanti/fp-ts) repository. 70 | 71 | To illustrate the power of `docs-ts`, here is a small example. Running `npm run docs-ts` (or `yarn docs-ts`) in the root directory of a project containing the following file in the `src` directory... 72 | 73 | ```ts 74 | /** 75 | * @since 0.2.0 76 | */ 77 | import { log } from 'fp-ts/Console' 78 | import { IO } from 'fp-ts/IO' 79 | 80 | /** 81 | * Greets a person by name. 82 | * 83 | * @example 84 | * import { sayHello } from 'docs-ts/lib/greetings' 85 | * 86 | * assert.strictEqual(sayHello('Test')(), 'Hello, Test!') 87 | * // => This assert statement will be run by docs-ts so you can test your code on-the-fly. 88 | * 89 | * @category greetings 90 | * @since 0.6.0 91 | */ 92 | export const sayHello = (name: string): IO => log(`Hello, ${name}!`) 93 | ``` 94 | 95 | ...will, by default, produce a `docs` directory containing the following markdown document in the `modules` subfolder. 96 | 97 | ````md 98 | --- 99 | title: greetings.ts 100 | nav_order: 0 101 | parent: Modules 102 | --- 103 | 104 | ## greetings overview 105 | 106 | Added in v0.2.0 107 | 108 | --- 109 | 110 |

Table of contents

111 | 112 | - [greetings](#greetings) 113 | - [sayHello](#sayhello) 114 | 115 | --- 116 | 117 | # greetings 118 | 119 | ## sayHello 120 | 121 | Greets a person by name. 122 | 123 | **Signature** 124 | 125 | ```ts 126 | export declare const sayHello: (name: string) => IO 127 | ``` 128 | 129 | **Example** 130 | 131 | ```ts 132 | import { sayHello } from 'docs-ts/lib/greetings' 133 | 134 | assert.strictEqual(sayHello('Test')(), 'Hello, Test!') 135 | ``` 136 | 137 | Added in v0.6.0 138 | ```` 139 | 140 | ## Configuration 141 | 142 | `docs-ts` is meant to be a zero-configuration command-line tool by default. However, there are several configuration settings that can be specified for `docs-ts`. To customize the configuration of `docs-ts`, create a `docs-ts.json` file in the root directory of your project and indicate the custom configuration parameters that the tool should use when generating documentation. 143 | 144 | The `docs-ts.json` configuration file adheres to the following interface: 145 | 146 | ```ts 147 | interface Config { 148 | readonly projectHomepage?: string 149 | readonly srcDir?: string 150 | readonly outDir?: string 151 | readonly theme?: string 152 | readonly enableSearch?: boolean 153 | readonly enforceDescriptions?: boolean 154 | readonly enforceExamples?: boolean 155 | readonly enforceVersion?: boolean 156 | readonly exclude?: ReadonlyArray 157 | readonly compilerOptions?: Record 158 | } 159 | ``` 160 | 161 | The following table describes each configuration parameter, its purpose, and its default value. 162 | 163 | | Parameter | Description | Default Value | 164 | | :---------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------- | 165 | | projectHomepage | Will link to the project homepage from the [Auxiliary Links](https://pmarsceill.github.io/just-the-docs/docs/navigation-structure/#auxiliary-links) of the generated documentation. | `homepage` in `package.json` | 166 | | srcDir | The directory in which `docs-ts` will search for TypeScript files to parse. | `'src'` | 167 | | outDir | The directory to which `docs-ts` will generate its output markdown documents. | `'docs'` | 168 | | theme | The theme that `docs-ts` will specify should be used for GitHub Docs in the generated `_config.yml` file. | `'pmarsceill/just-the-docs'` | 169 | | enableSearch | Whether or search should be enabled for GitHub Docs in the generated `_config.yml` file. | `true` | 170 | | enforceDescriptions | Whether or not descriptions for each module export should be required. | `false` | 171 | | enforceExamples | Whether or not `@example` tags for each module export should be required. (**Note**: examples will not be enforced in module documentation) | `false` | 172 | | enforceVersion | Whether or not `@since` tags for each module export should be required. | `true` | 173 | | exclude | An array of glob strings specifying files that should be excluded from the documentation. | `[]` | 174 | | parseCompilerOptions | tsconfig for parsing options | {} | 175 | | examplesCompilerOptions | tsconfig for the examples options | {} | 176 | 177 | ## Documentation 178 | 179 | - [Docs](https://gcanti.github.io/docs-ts) 180 | 181 | ## FAQ 182 | 183 | **Q:** For functions that have overloaded definitions, is it possible to document each overload separately? 184 | 185 | **A:** No, `docs-ts` will use the documentation provided for the first overload of a function in its generated output. 186 | 187 | ## License 188 | 189 | The MIT License (MIT) 190 | -------------------------------------------------------------------------------- /test/Markdown.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import * as O from 'fp-ts/Option' 3 | import * as RA from 'fp-ts/ReadonlyArray' 4 | 5 | import * as _ from '../src/Markdown' 6 | import { 7 | Class, 8 | Constant, 9 | Documentable, 10 | Export, 11 | Function, 12 | Interface, 13 | Method, 14 | Module, 15 | Property, 16 | TypeAlias 17 | } from '../src/Module' 18 | 19 | const content = _.PlainText('a') 20 | 21 | const testCases = { 22 | class: Class( 23 | Documentable('A', O.some('a class'), O.some('1.0.0'), false, ['example 1'], O.some('category')), 24 | 'declare class A { constructor() }', 25 | [ 26 | Method(Documentable('hasOwnProperty', O.none, O.some('1.0.0'), false, RA.empty, O.none), [ 27 | 'hasOwnProperty(): boolean' 28 | ]) 29 | ], 30 | [ 31 | Method(Documentable('staticTest', O.none, O.some('1.0.0'), false, RA.empty, O.none), [ 32 | 'static testStatic(): string;' 33 | ]) 34 | ], 35 | [Property(Documentable('foo', O.none, O.some('1.0.0'), false, RA.empty, O.none), 'foo: string')] 36 | ), 37 | constant: Constant( 38 | Documentable('test', O.some('the test'), O.some('1.0.0'), false, RA.empty, O.some('constants')), 39 | 'declare const test: string' 40 | ), 41 | export: Export( 42 | Documentable('test', O.none, O.some('1.0.0'), false, RA.empty, O.none), 43 | 'export declare const test: typeof test' 44 | ), 45 | function: Function(Documentable('func', O.some('a function'), O.some('1.0.0'), true, ['example 1'], O.none), [ 46 | 'declare const func: (test: string) => string' 47 | ]), 48 | interface: Interface( 49 | Documentable('A', O.none, O.some('1.0.0'), false, RA.empty, O.none), 50 | 'export interface A extends Record {}' 51 | ), 52 | typeAlias: TypeAlias(Documentable('A', O.none, O.some('1.0.0'), false, RA.empty, O.none), 'export type A = number') 53 | } 54 | 55 | describe.concurrent('Markdown', () => { 56 | describe.concurrent('constructors', () => { 57 | it('Bold', () => { 58 | assert.deepStrictEqual(_.Bold(content), { 59 | _tag: 'Bold', 60 | content 61 | }) 62 | }) 63 | 64 | it('Fence', () => { 65 | assert.deepStrictEqual(_.Fence('ts', content), { 66 | _tag: 'Fence', 67 | language: 'ts', 68 | content 69 | }) 70 | }) 71 | 72 | it('Header', () => { 73 | assert.deepStrictEqual(_.Header(1, content), { 74 | _tag: 'Header', 75 | level: 1, 76 | content 77 | }) 78 | }) 79 | 80 | it('Newline', () => { 81 | assert.deepStrictEqual(_.Newline, { 82 | _tag: 'Newline' 83 | }) 84 | }) 85 | 86 | it('Paragraph', () => { 87 | assert.deepStrictEqual(_.Paragraph(content), { 88 | _tag: 'Paragraph', 89 | content 90 | }) 91 | }) 92 | 93 | it('PlainText', () => { 94 | assert.deepStrictEqual(_.PlainText('a'), { 95 | _tag: 'PlainText', 96 | content: 'a' 97 | }) 98 | }) 99 | 100 | it('PlainTexts', () => { 101 | assert.deepStrictEqual(_.PlainTexts([content]), { 102 | _tag: 'PlainTexts', 103 | content: [content] 104 | }) 105 | }) 106 | 107 | it('Strikethrough', () => { 108 | assert.deepStrictEqual(_.Strikethrough(content), { 109 | _tag: 'Strikethrough', 110 | content 111 | }) 112 | }) 113 | }) 114 | 115 | describe.concurrent('destructors', () => { 116 | it('fold', () => { 117 | const fold: (markdown: _.Markdown) => string = _.fold({ 118 | Bold: (c) => `Bold(${fold(c)})`, 119 | Fence: (l, c) => `Fence(${l}, ${fold(c)})`, 120 | Header: (l, c) => `Header(${l}, ${fold(c)})`, 121 | Newline: () => `Newline`, 122 | Paragraph: (c) => `Paragraph(${fold(c)})`, 123 | PlainText: (s) => s, 124 | PlainTexts: (cs) => `PlainTexts(${RA.getShow({ show: fold }).show(cs)})`, 125 | Strikethrough: (c) => `Strikethrough(${fold(c)})` 126 | }) 127 | 128 | assert.strictEqual(fold(_.Bold(content)), 'Bold(a)') 129 | assert.strictEqual(fold(_.Fence('ts', content)), 'Fence(ts, a)') 130 | assert.strictEqual(fold(_.Header(1, content)), 'Header(1, a)') 131 | assert.strictEqual(fold(_.Newline), 'Newline') 132 | assert.strictEqual(fold(_.Paragraph(content)), 'Paragraph(a)') 133 | assert.strictEqual(fold(_.PlainText('a')), 'a') 134 | assert.strictEqual(fold(_.PlainTexts([content])), 'PlainTexts([a])') 135 | assert.strictEqual(fold(_.Strikethrough(content)), 'Strikethrough(a)') 136 | assert.throws(() => { 137 | // @ts-expect-error - valid Markdown instance required 138 | fold({}) 139 | }) 140 | }) 141 | }) 142 | 143 | describe.concurrent('instances', () => { 144 | it('semigroupMarkdown', () => { 145 | assert.deepStrictEqual(_.semigroupMarkdown.concat(_.Bold(content), _.Strikethrough(content)), { 146 | _tag: 'PlainTexts', 147 | content: [ 148 | { _tag: 'Bold', content: { _tag: 'PlainText', content: 'a' } }, 149 | { _tag: 'Strikethrough', content: { _tag: 'PlainText', content: 'a' } } 150 | ] 151 | }) 152 | }) 153 | 154 | it('monoidMarkdown', () => { 155 | assert.deepStrictEqual(_.monoidMarkdown.empty, { 156 | _tag: 'PlainText', 157 | content: '' 158 | }) 159 | 160 | assert.deepStrictEqual(_.monoidMarkdown.concat(_.Bold(content), _.Strikethrough(content)), { 161 | _tag: 'PlainTexts', 162 | content: [ 163 | { _tag: 'Bold', content: { _tag: 'PlainText', content: 'a' } }, 164 | { _tag: 'Strikethrough', content: { _tag: 'PlainText', content: 'a' } } 165 | ] 166 | }) 167 | }) 168 | 169 | it('showMarkdown', () => { 170 | // Prettier will add a trailing newline to the document so `a` becomes `\n` 171 | // and strips extra trailing newlines so `\n\n` becomes `\n` 172 | assert.strictEqual(_.showMarkdown.show(_.Bold(content)), '**a**\n') 173 | assert.strictEqual(_.showMarkdown.show(_.Header(1, content)), '# a\n') 174 | assert.strictEqual(_.showMarkdown.show(_.Header(2, content)), '## a\n') 175 | assert.strictEqual(_.showMarkdown.show(_.Header(3, content)), '### a\n') 176 | assert.strictEqual(_.showMarkdown.show(_.Fence('ts', content)), '```ts\na\n```\n') 177 | assert.strictEqual(_.showMarkdown.show(_.monoidMarkdown.concat(content, _.Newline)), 'a\n') 178 | assert.strictEqual(_.showMarkdown.show(_.Paragraph(content)), 'a\n') 179 | assert.strictEqual(_.showMarkdown.show(_.PlainText('a')), 'a\n') 180 | assert.strictEqual(_.showMarkdown.show(_.PlainTexts([content, _.Newline, content])), 'a\na\n') 181 | assert.strictEqual(_.showMarkdown.show(_.Strikethrough(content)), '~~a~~\n') 182 | assert.strictEqual( 183 | _.showMarkdown.show( 184 | _.PlainTexts([ 185 | _.PlainText(''), 186 | _.Bold(content), 187 | _.Header(1, content), 188 | _.Fence('ts', content), 189 | _.Newline, 190 | _.Paragraph(content), 191 | _.PlainText('a'), 192 | _.PlainTexts([content]), 193 | _.Strikethrough(content) 194 | ]) 195 | ), 196 | `**a** 197 | 198 | # a 199 | 200 | \`\`\`ts 201 | a 202 | \`\`\` 203 | 204 | a 205 | 206 | aa~~a~~ 207 | ` 208 | ) 209 | }) 210 | }) 211 | 212 | describe.concurrent('printers', () => { 213 | it('printClass', () => { 214 | assert.strictEqual( 215 | _.printClass(testCases.class), 216 | `## A (class) 217 | 218 | a class 219 | 220 | **Signature** 221 | 222 | \`\`\`ts 223 | declare class A { 224 | constructor() 225 | } 226 | \`\`\` 227 | 228 | **Example** 229 | 230 | \`\`\`ts 231 | example 1 232 | \`\`\` 233 | 234 | Added in v1.0.0 235 | 236 | ### staticTest (static method) 237 | 238 | **Signature** 239 | 240 | \`\`\`ts 241 | static testStatic(): string; 242 | \`\`\` 243 | 244 | Added in v1.0.0 245 | 246 | ### hasOwnProperty (function) (method) 247 | 248 | **Signature** 249 | 250 | \`\`\`ts 251 | hasOwnProperty(): boolean 252 | \`\`\` 253 | 254 | Added in v1.0.0 255 | 256 | ### foo (property) 257 | 258 | **Signature** 259 | 260 | \`\`\`ts 261 | foo: string 262 | \`\`\` 263 | 264 | Added in v1.0.0 265 | ` 266 | ) 267 | }) 268 | 269 | it('printConstant', () => { 270 | assert.strictEqual( 271 | _.printConstant(testCases.constant), 272 | `## test 273 | 274 | the test 275 | 276 | **Signature** 277 | 278 | \`\`\`ts 279 | declare const test: string 280 | \`\`\` 281 | 282 | Added in v1.0.0 283 | ` 284 | ) 285 | }) 286 | 287 | it('printExport', () => { 288 | assert.strictEqual( 289 | _.printExport(testCases.export), 290 | `## test 291 | 292 | **Signature** 293 | 294 | \`\`\`ts 295 | export declare const test: typeof test 296 | \`\`\` 297 | 298 | Added in v1.0.0 299 | ` 300 | ) 301 | }) 302 | 303 | it('printFunction', () => { 304 | assert.strictEqual( 305 | _.printFunction(testCases.function), 306 | `## ~~func~~ 307 | 308 | a function 309 | 310 | **Signature** 311 | 312 | \`\`\`ts 313 | declare const func: (test: string) => string 314 | \`\`\` 315 | 316 | **Example** 317 | 318 | \`\`\`ts 319 | example 1 320 | \`\`\` 321 | 322 | Added in v1.0.0 323 | ` 324 | ) 325 | }) 326 | 327 | it('printInterface', () => { 328 | assert.strictEqual( 329 | _.printInterface(testCases.interface), 330 | `## A (interface) 331 | 332 | **Signature** 333 | 334 | \`\`\`ts 335 | export interface A extends Record {} 336 | \`\`\` 337 | 338 | Added in v1.0.0 339 | ` 340 | ) 341 | }) 342 | 343 | it('printTypeAlias', () => { 344 | assert.strictEqual( 345 | _.printTypeAlias(testCases.typeAlias), 346 | `## A (type alias) 347 | 348 | **Signature** 349 | 350 | \`\`\`ts 351 | export type A = number 352 | \`\`\` 353 | 354 | Added in v1.0.0 355 | ` 356 | ) 357 | 358 | assert.strictEqual( 359 | _.printTypeAlias({ ...testCases.typeAlias, since: O.none }), 360 | `## A (type alias) 361 | 362 | **Signature** 363 | 364 | \`\`\`ts 365 | export type A = number 366 | \`\`\` 367 | ` 368 | ) 369 | }) 370 | 371 | it('printModule', () => { 372 | const documentation = Documentable('tests', O.none, O.some('1.0.0'), false, RA.empty, O.none) 373 | const m = Module( 374 | documentation, 375 | ['src', 'tests.ts'], 376 | [testCases.class], 377 | [testCases.interface], 378 | [testCases.function], 379 | [testCases.typeAlias], 380 | [testCases.constant], 381 | [testCases.export] 382 | ) 383 | 384 | assert.strictEqual( 385 | _.printModule(m, 1), 386 | `--- 387 | title: tests.ts 388 | nav_order: 1 389 | parent: Modules 390 | --- 391 | 392 | ## tests overview 393 | 394 | Added in v1.0.0 395 | 396 | --- 397 | 398 |

Table of contents

399 | 400 | - [category](#category) 401 | - [A (class)](#a-class) 402 | - [staticTest (static method)](#statictest-static-method) 403 | - [hasOwnProperty (function) (method)](#hasownproperty-function-method) 404 | - [foo (property)](#foo-property) 405 | - [constants](#constants) 406 | - [test](#test) 407 | - [utils](#utils) 408 | - [A (interface)](#a-interface) 409 | - [A (type alias)](#a-type-alias) 410 | - [test](#test-1) 411 | - [~~func~~](#func) 412 | 413 | --- 414 | 415 | # category 416 | 417 | ## A (class) 418 | 419 | a class 420 | 421 | **Signature** 422 | 423 | \`\`\`ts 424 | declare class A { 425 | constructor() 426 | } 427 | \`\`\` 428 | 429 | **Example** 430 | 431 | \`\`\`ts 432 | example 1 433 | \`\`\` 434 | 435 | Added in v1.0.0 436 | 437 | ### staticTest (static method) 438 | 439 | **Signature** 440 | 441 | \`\`\`ts 442 | static testStatic(): string; 443 | \`\`\` 444 | 445 | Added in v1.0.0 446 | 447 | ### hasOwnProperty (function) (method) 448 | 449 | **Signature** 450 | 451 | \`\`\`ts 452 | hasOwnProperty(): boolean 453 | \`\`\` 454 | 455 | Added in v1.0.0 456 | 457 | ### foo (property) 458 | 459 | **Signature** 460 | 461 | \`\`\`ts 462 | foo: string 463 | \`\`\` 464 | 465 | Added in v1.0.0 466 | 467 | # constants 468 | 469 | ## test 470 | 471 | the test 472 | 473 | **Signature** 474 | 475 | \`\`\`ts 476 | declare const test: string 477 | \`\`\` 478 | 479 | Added in v1.0.0 480 | 481 | # utils 482 | 483 | ## A (interface) 484 | 485 | **Signature** 486 | 487 | \`\`\`ts 488 | export interface A extends Record {} 489 | \`\`\` 490 | 491 | Added in v1.0.0 492 | 493 | ## A (type alias) 494 | 495 | **Signature** 496 | 497 | \`\`\`ts 498 | export type A = number 499 | \`\`\` 500 | 501 | Added in v1.0.0 502 | 503 | ## test 504 | 505 | **Signature** 506 | 507 | \`\`\`ts 508 | export declare const test: typeof test 509 | \`\`\` 510 | 511 | Added in v1.0.0 512 | 513 | ## ~~func~~ 514 | 515 | a function 516 | 517 | **Signature** 518 | 519 | \`\`\`ts 520 | declare const func: (test: string) => string 521 | \`\`\` 522 | 523 | **Example** 524 | 525 | \`\`\`ts 526 | example 1 527 | \`\`\` 528 | 529 | Added in v1.0.0 530 | ` 531 | ) 532 | 533 | const empty = Module( 534 | documentation, 535 | ['src', 'tests.ts'], 536 | RA.empty, 537 | RA.empty, 538 | RA.empty, 539 | RA.empty, 540 | RA.empty, 541 | RA.empty 542 | ) 543 | 544 | assert.strictEqual( 545 | _.printModule(empty, 1), 546 | `--- 547 | title: tests.ts 548 | nav_order: 1 549 | parent: Modules 550 | --- 551 | 552 | ## tests overview 553 | 554 | Added in v1.0.0 555 | 556 | --- 557 | 558 |

Table of contents

559 | 560 | --- 561 | ` 562 | ) 563 | 564 | const throws = Module( 565 | documentation, 566 | ['src', 'tests.ts'], 567 | // @ts-expect-error - valid Markdown instance required 568 | [{ category: 'invalid markdown' }], 569 | RA.empty, 570 | RA.empty, 571 | RA.empty, 572 | RA.empty, 573 | RA.empty 574 | ) 575 | 576 | assert.throws(() => { 577 | _.printModule(throws, 1) 578 | }) 579 | }) 580 | }) 581 | }) 582 | -------------------------------------------------------------------------------- /src/Markdown.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 0.6.0 3 | */ 4 | import { Endomorphism } from 'fp-ts/Endomorphism' 5 | import { intercalate } from 'fp-ts/Foldable' 6 | import { absurd, flow, pipe } from 'fp-ts/function' 7 | import * as M from 'fp-ts/Monoid' 8 | import * as O from 'fp-ts/Option' 9 | import * as RA from 'fp-ts/ReadonlyArray' 10 | import * as RNEA from 'fp-ts/ReadonlyNonEmptyArray' 11 | import * as RR from 'fp-ts/ReadonlyRecord' 12 | import { Semigroup } from 'fp-ts/Semigroup' 13 | import { Show } from 'fp-ts/Show' 14 | import * as S from 'fp-ts/string' 15 | import * as prettier from 'prettier' 16 | 17 | import { Class, Constant, Export, Function, Interface, Method, Module, Property, TypeAlias } from './Module' 18 | 19 | // eslint-disable-next-line @typescript-eslint/no-var-requires 20 | const toc = require('markdown-toc') 21 | 22 | // ------------------------------------------------------------------------------------- 23 | // model 24 | // ------------------------------------------------------------------------------------- 25 | 26 | /** 27 | * @category model 28 | * @since 0.6.0 29 | */ 30 | export type Printable = Class | Constant | Export | Function | Interface | TypeAlias 31 | 32 | /** 33 | * @category model 34 | * @since 0.6.0 35 | */ 36 | export type Markdown = Bold | Fence | Header | Newline | Paragraph | PlainText | PlainTexts | Strikethrough 37 | 38 | /** 39 | * @category model 40 | * @since 0.6.0 41 | */ 42 | export interface Bold { 43 | readonly _tag: 'Bold' 44 | readonly content: Markdown 45 | } 46 | 47 | /** 48 | * @category model 49 | * @since 0.6.0 50 | */ 51 | export interface Fence { 52 | readonly _tag: 'Fence' 53 | readonly language: string 54 | readonly content: Markdown 55 | } 56 | 57 | /** 58 | * @category model 59 | * @since 0.6.0 60 | */ 61 | export interface Header { 62 | readonly _tag: 'Header' 63 | readonly level: number 64 | readonly content: Markdown 65 | } 66 | 67 | /** 68 | * @category model 69 | * @since 0.6.0 70 | */ 71 | export interface Newline { 72 | readonly _tag: 'Newline' 73 | } 74 | 75 | /** 76 | * @category model 77 | * @since 0.6.0 78 | */ 79 | export interface Paragraph { 80 | readonly _tag: 'Paragraph' 81 | readonly content: Markdown 82 | } 83 | 84 | /** 85 | * @category model 86 | * @since 0.6.0 87 | */ 88 | export interface PlainText { 89 | readonly _tag: 'PlainText' 90 | readonly content: string 91 | } 92 | 93 | /** 94 | * @category model 95 | * @since 0.6.0 96 | */ 97 | export interface PlainTexts { 98 | readonly _tag: 'PlainTexts' 99 | readonly content: ReadonlyArray 100 | } 101 | /** 102 | * @category model 103 | * @since 0.6.0 104 | */ 105 | export interface Strikethrough { 106 | readonly _tag: 'Strikethrough' 107 | readonly content: Markdown 108 | } 109 | 110 | // ------------------------------------------------------------------------------------- 111 | // constructors 112 | // ------------------------------------------------------------------------------------- 113 | 114 | /** 115 | * @category constructors 116 | * @since 0.6.0 117 | */ 118 | export const Bold = (content: Markdown): Markdown => ({ 119 | _tag: 'Bold', 120 | content 121 | }) 122 | 123 | /** 124 | * @category constructors 125 | * @since 0.6.0 126 | */ 127 | export const Fence = (language: string, content: Markdown): Markdown => ({ 128 | _tag: 'Fence', 129 | language, 130 | content 131 | }) 132 | 133 | /** 134 | * @category constructors 135 | * @since 0.6.0 136 | */ 137 | export const Header = (level: number, content: Markdown): Markdown => ({ 138 | _tag: 'Header', 139 | level, 140 | content 141 | }) 142 | 143 | /** 144 | * @category constructors 145 | * @since 0.6.0 146 | */ 147 | export const Newline: Markdown = { 148 | _tag: 'Newline' 149 | } 150 | 151 | /** 152 | * @category constructors 153 | * @since 0.6.0 154 | */ 155 | export const Paragraph = (content: Markdown): Markdown => ({ 156 | _tag: 'Paragraph', 157 | content 158 | }) 159 | 160 | /** 161 | * @category constructors 162 | * @since 0.6.0 163 | */ 164 | export const PlainText = (content: string): Markdown => ({ 165 | _tag: 'PlainText', 166 | content 167 | }) 168 | 169 | /** 170 | * @category constructors 171 | * @since 0.6.0 172 | */ 173 | export const PlainTexts = (content: ReadonlyArray): Markdown => ({ 174 | _tag: 'PlainTexts', 175 | content 176 | }) 177 | 178 | /** 179 | * @category constructors 180 | * @since 0.6.0 181 | */ 182 | export const Strikethrough = (content: Markdown): Markdown => ({ 183 | _tag: 'Strikethrough', 184 | content 185 | }) 186 | 187 | // ------------------------------------------------------------------------------------- 188 | // destructors 189 | // ------------------------------------------------------------------------------------- 190 | 191 | /** 192 | * @category destructors 193 | * @since 0.6.0 194 | */ 195 | export const fold = (patterns: { 196 | readonly Bold: (content: Markdown) => R 197 | readonly Fence: (language: string, content: Markdown) => R 198 | readonly Header: (level: number, content: Markdown) => R 199 | readonly Newline: () => R 200 | readonly Paragraph: (content: Markdown) => R 201 | readonly PlainText: (content: string) => R 202 | readonly PlainTexts: (content: ReadonlyArray) => R 203 | readonly Strikethrough: (content: Markdown) => R 204 | }): ((markdown: Markdown) => R) => { 205 | const f = (x: Markdown): R => { 206 | switch (x._tag) { 207 | case 'Bold': 208 | return patterns.Bold(x.content) 209 | case 'Fence': 210 | return patterns.Fence(x.language, x.content) 211 | case 'Header': 212 | return patterns.Header(x.level, x.content) 213 | case 'Newline': 214 | return patterns.Newline() 215 | case 'Paragraph': 216 | return patterns.Paragraph(x.content) 217 | case 'PlainText': 218 | return patterns.PlainText(x.content) 219 | case 'PlainTexts': 220 | return patterns.PlainTexts(x.content) 221 | case 'Strikethrough': 222 | return patterns.Strikethrough(x.content) 223 | default: 224 | return absurd(x) 225 | } 226 | } 227 | return f 228 | } 229 | 230 | // ------------------------------------------------------------------------------------- 231 | // combinators 232 | // ------------------------------------------------------------------------------------- 233 | 234 | const foldS: (as: ReadonlyArray) => string = M.concatAll(S.Monoid) 235 | 236 | const foldMarkdown = (as: ReadonlyArray): Markdown => pipe(as, M.concatAll(monoidMarkdown)) 237 | 238 | const CRLF: Markdown = PlainTexts(RA.replicate(2, Newline)) 239 | 240 | const intercalateCRLF = (xs: ReadonlyArray): Markdown => intercalate(monoidMarkdown, RA.Foldable)(CRLF, xs) 241 | 242 | const intercalateNewline = (xs: ReadonlyArray): string => intercalate(S.Monoid, RA.Foldable)('\n', xs) 243 | 244 | const h1 = (content: Markdown) => Header(1, content) 245 | 246 | const h2 = (content: Markdown) => Header(2, content) 247 | 248 | const h3 = (content: Markdown) => Header(3, content) 249 | 250 | const ts = (code: string) => Fence('ts', PlainText(code)) 251 | 252 | const since: (v: O.Option) => Markdown = O.fold( 253 | () => monoidMarkdown.empty, 254 | (v) => foldMarkdown([CRLF, PlainText(`Added in v${v}`)]) 255 | ) 256 | 257 | const title = (s: string, deprecated: boolean, type?: string): Markdown => { 258 | const title = s.trim() === 'hasOwnProperty' ? `${s} (function)` : s 259 | const markdownTitle = deprecated ? Strikethrough(PlainText(title)) : PlainText(title) 260 | return pipe( 261 | O.fromNullable(type), 262 | O.fold( 263 | () => markdownTitle, 264 | (t) => foldMarkdown([markdownTitle, PlainText(` ${t}`)]) 265 | ) 266 | ) 267 | } 268 | 269 | const description: (d: O.Option) => Markdown = flow( 270 | O.fold(() => monoidMarkdown.empty, PlainText), 271 | Paragraph 272 | ) 273 | 274 | const signature = (s: string): Markdown => 275 | pipe(RA.of(ts(s)), RA.prepend(Paragraph(Bold(PlainText('Signature')))), foldMarkdown) 276 | 277 | const signatures = (ss: ReadonlyArray): Markdown => 278 | pipe(RA.of(ts(intercalateNewline(ss))), RA.prepend(Paragraph(Bold(PlainText('Signature')))), foldMarkdown) 279 | 280 | const examples: (es: ReadonlyArray) => Markdown = flow( 281 | RA.map((code) => pipe(RA.of(ts(code)), RA.prepend(Bold(PlainText('Example'))), intercalateCRLF)), 282 | intercalateCRLF 283 | ) 284 | 285 | const staticMethod = (m: Method): Markdown => 286 | Paragraph( 287 | foldMarkdown([ 288 | h3(title(m.name, m.deprecated, '(static method)')), 289 | description(m.description), 290 | signatures(m.signatures), 291 | examples(m.examples), 292 | since(m.since) 293 | ]) 294 | ) 295 | 296 | const method = (m: Method): Markdown => 297 | Paragraph( 298 | foldMarkdown([ 299 | h3(title(m.name, m.deprecated, '(method)')), 300 | description(m.description), 301 | signatures(m.signatures), 302 | examples(m.examples), 303 | since(m.since) 304 | ]) 305 | ) 306 | 307 | const propertyToMarkdown = (p: Property): Markdown => 308 | Paragraph( 309 | foldMarkdown([ 310 | h3(title(p.name, p.deprecated, '(property)')), 311 | description(p.description), 312 | signature(p.signature), 313 | examples(p.examples), 314 | since(p.since) 315 | ]) 316 | ) 317 | 318 | const staticMethods: (ms: ReadonlyArray) => Markdown = flow(RA.map(staticMethod), intercalateCRLF) 319 | 320 | const methods: (methods: ReadonlyArray) => Markdown = flow(RA.map(method), intercalateCRLF) 321 | 322 | const properties: (properties: ReadonlyArray) => Markdown = flow(RA.map(propertyToMarkdown), intercalateCRLF) 323 | 324 | const moduleDescription = (m: Module): Markdown => 325 | Paragraph( 326 | foldMarkdown([ 327 | Paragraph(h2(title(m.name, m.deprecated, 'overview'))), 328 | description(m.description), 329 | examples(m.examples), 330 | since(m.since) 331 | ]) 332 | ) 333 | 334 | const meta = (title: string, order: number): Markdown => 335 | Paragraph( 336 | foldMarkdown([ 337 | PlainText('---'), 338 | Newline, 339 | PlainText(`title: ${title}`), 340 | Newline, 341 | PlainText(`nav_order: ${order}`), 342 | Newline, 343 | PlainText(`parent: Modules`), 344 | Newline, 345 | PlainText('---') 346 | ]) 347 | ) 348 | 349 | const fromClass = (c: Class): Markdown => 350 | Paragraph( 351 | foldMarkdown([ 352 | Paragraph( 353 | foldMarkdown([ 354 | h2(title(c.name, c.deprecated, '(class)')), 355 | description(c.description), 356 | signature(c.signature), 357 | examples(c.examples), 358 | since(c.since) 359 | ]) 360 | ), 361 | staticMethods(c.staticMethods), 362 | methods(c.methods), 363 | properties(c.properties) 364 | ]) 365 | ) 366 | 367 | const fromConstant = (c: Constant): Markdown => 368 | Paragraph( 369 | foldMarkdown([ 370 | h2(title(c.name, c.deprecated)), 371 | description(c.description), 372 | signature(c.signature), 373 | examples(c.examples), 374 | since(c.since) 375 | ]) 376 | ) 377 | 378 | const fromExport = (e: Export): Markdown => 379 | Paragraph( 380 | foldMarkdown([ 381 | h2(title(e.name, e.deprecated)), 382 | description(e.description), 383 | signature(e.signature), 384 | examples(e.examples), 385 | since(e.since) 386 | ]) 387 | ) 388 | 389 | const fromFunction = (f: Function): Markdown => 390 | Paragraph( 391 | foldMarkdown([ 392 | h2(title(f.name, f.deprecated)), 393 | description(f.description), 394 | signatures(f.signatures), 395 | examples(f.examples), 396 | since(f.since) 397 | ]) 398 | ) 399 | 400 | const fromInterface = (i: Interface): Markdown => 401 | Paragraph( 402 | foldMarkdown([ 403 | h2(title(i.name, i.deprecated, '(interface)')), 404 | description(i.description), 405 | signature(i.signature), 406 | examples(i.examples), 407 | since(i.since) 408 | ]) 409 | ) 410 | 411 | const fromTypeAlias = (ta: TypeAlias): Markdown => 412 | Paragraph( 413 | foldMarkdown([ 414 | h2(title(ta.name, ta.deprecated, '(type alias)')), 415 | description(ta.description), 416 | signature(ta.signature), 417 | examples(ta.examples), 418 | since(ta.since) 419 | ]) 420 | ) 421 | 422 | const fromPrintable = (p: Printable): Markdown => { 423 | switch (p._tag) { 424 | case 'Class': 425 | return fromClass(p) 426 | case 'Constant': 427 | return fromConstant(p) 428 | case 'Export': 429 | return fromExport(p) 430 | case 'Function': 431 | return fromFunction(p) 432 | case 'Interface': 433 | return fromInterface(p) 434 | case 'TypeAlias': 435 | return fromTypeAlias(p) 436 | default: 437 | return absurd(p) 438 | } 439 | } 440 | 441 | // ------------------------------------------------------------------------------------- 442 | // printers 443 | // ------------------------------------------------------------------------------------- 444 | 445 | const getPrintables = (module: Module): O.Option> => 446 | pipe( 447 | M.concatAll(RA.getMonoid())([ 448 | module.classes, 449 | module.constants, 450 | module.exports, 451 | module.functions, 452 | module.interfaces, 453 | module.typeAliases 454 | ]), 455 | RNEA.fromReadonlyArray 456 | ) 457 | 458 | /** 459 | * @category printers 460 | * @since 0.6.0 461 | */ 462 | export const printClass = (c: Class): string => pipe(fromClass(c), showMarkdown.show) 463 | 464 | /** 465 | * @category printers 466 | * @since 0.6.0 467 | */ 468 | export const printConstant = (c: Constant): string => pipe(fromConstant(c), showMarkdown.show) 469 | 470 | /** 471 | * @category printers 472 | * @since 0.6.0 473 | */ 474 | export const printExport = (e: Export): string => pipe(fromExport(e), showMarkdown.show) 475 | 476 | /** 477 | * @category printers 478 | * @since 0.6.0 479 | */ 480 | export const printFunction = (f: Function): string => pipe(fromFunction(f), showMarkdown.show) 481 | 482 | /** 483 | * @category printers 484 | * @since 0.6.0 485 | */ 486 | export const printInterface = (i: Interface): string => pipe(fromInterface(i), showMarkdown.show) 487 | 488 | /** 489 | * @category printers 490 | * @since 0.6.0 491 | */ 492 | export const printTypeAlias = (f: TypeAlias): string => pipe(fromTypeAlias(f), showMarkdown.show) 493 | 494 | /** 495 | * @category printers 496 | * @since 0.6.0 497 | */ 498 | export const printModule = (module: Module, order: number): string => { 499 | const DEFAULT_CATEGORY = 'utils' 500 | 501 | const header = pipe(meta(module.path.slice(1).join('/'), order), showMarkdown.show) 502 | 503 | const description = pipe(Paragraph(moduleDescription(module)), showMarkdown.show) 504 | 505 | const content = pipe( 506 | getPrintables(module), 507 | O.map( 508 | flow( 509 | RNEA.groupBy(({ category }) => 510 | pipe( 511 | category, 512 | O.getOrElse(() => DEFAULT_CATEGORY) 513 | ) 514 | ), 515 | RR.collect(S.Ord)((category, printables) => { 516 | const title = pipe(h1(PlainText(category)), showMarkdown.show) 517 | const documentation = pipe( 518 | printables, 519 | RA.map(flow(fromPrintable, showMarkdown.show)), 520 | RA.sort(S.Ord), 521 | intercalateNewline 522 | ) 523 | return intercalateNewline([title, documentation]) 524 | }), 525 | RA.sort(S.Ord), 526 | intercalateNewline 527 | ) 528 | ), 529 | O.getOrElse(() => '') 530 | ) 531 | 532 | const tableOfContents = (c: string): string => 533 | pipe( 534 | Paragraph( 535 | foldMarkdown([Paragraph(PlainText('

Table of contents

')), PlainText(toc(c).content)]) 536 | ), 537 | showMarkdown.show 538 | ) 539 | 540 | return pipe(intercalateNewline([header, description, '---\n', tableOfContents(content), '---\n', content]), prettify) 541 | } 542 | 543 | // ------------------------------------------------------------------------------------- 544 | // instances 545 | // ------------------------------------------------------------------------------------- 546 | 547 | /** 548 | * @category instances 549 | * @since 0.6.0 550 | */ 551 | export const semigroupMarkdown: Semigroup = { 552 | concat: (x, y) => PlainTexts([x, y]) 553 | } 554 | 555 | /** 556 | * @category instances 557 | * @since 0.6.0 558 | */ 559 | export const monoidMarkdown: M.Monoid = { 560 | ...semigroupMarkdown, 561 | empty: PlainText('') 562 | } 563 | 564 | const prettierOptions: prettier.Options = { 565 | parser: 'markdown', 566 | semi: false, 567 | singleQuote: true, 568 | printWidth: 120 569 | } 570 | 571 | const prettify = (s: string): string => prettier.format(s, prettierOptions) 572 | 573 | const canonicalizeMarkdown: Endomorphism> = RA.filterMap((markdown) => 574 | pipe( 575 | markdown, 576 | fold({ 577 | Bold: () => O.some(markdown), 578 | Header: () => O.some(markdown), 579 | Fence: () => O.some(markdown), 580 | Newline: () => O.some(markdown), 581 | Paragraph: () => O.some(markdown), 582 | PlainText: (content) => (content.length > 0 ? O.some(markdown) : O.none), 583 | PlainTexts: (content) => O.some(PlainTexts(canonicalizeMarkdown(content))), 584 | Strikethrough: () => O.some(markdown) 585 | }) 586 | ) 587 | ) 588 | 589 | const markdownToString: (markdown: Markdown) => string = fold({ 590 | Bold: (content) => foldS(['**', markdownToString(content), '**']), 591 | Header: (level, content) => foldS(['\n', foldS(RA.replicate(level, '#')), ' ', markdownToString(content), '\n\n']), 592 | Fence: (language, content) => foldS(['```', language, '\n', markdownToString(content), '\n', '```\n\n']), 593 | Newline: () => '\n', 594 | Paragraph: (content) => foldS([markdownToString(content), '\n\n']), 595 | PlainText: (content) => content, 596 | PlainTexts: (content) => pipe(content, canonicalizeMarkdown, RA.map(markdownToString), foldS), 597 | Strikethrough: (content) => foldS(['~~', markdownToString(content), '~~']) 598 | }) 599 | 600 | /** 601 | * @category instances 602 | * @since 0.6.0 603 | */ 604 | export const showMarkdown: Show = { 605 | show: flow(markdownToString, prettify) 606 | } 607 | -------------------------------------------------------------------------------- /src/Core.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 0.6.0 3 | */ 4 | import * as Either from 'fp-ts/Either' 5 | import { constVoid, flow, pipe } from 'fp-ts/function' 6 | import * as Json from 'fp-ts/Json' 7 | import * as Monoid from 'fp-ts/Monoid' 8 | import * as RTE from 'fp-ts/ReaderTaskEither' 9 | import * as ReadonlyArray from 'fp-ts/ReadonlyArray' 10 | import * as S from 'fp-ts/string' 11 | import * as TaskEither from 'fp-ts/TaskEither' 12 | import * as Decoder from 'io-ts/Decoder' 13 | import * as path from 'path' 14 | import * as ast from 'ts-morph' 15 | 16 | import * as Config from './Config' 17 | import { File, FileSystem } from './FileSystem' 18 | import { Logger } from './Logger' 19 | import { printModule } from './Markdown' 20 | import { Documentable, Module } from './Module' 21 | import * as Parser from './Parser' 22 | 23 | /** 24 | * @category main 25 | * @since 0.6.0 26 | */ 27 | export const main: Program = pipe( 28 | RTE.Do, 29 | RTE.bind('capabilities', () => RTE.ask()), 30 | RTE.bind('pkg', () => parsePackageJSON), 31 | RTE.bind('config', ({ pkg }) => getConfig(pkg.name, pkg.homepage)), 32 | RTE.chainTaskEitherK(({ config, capabilities }) => { 33 | const program = pipe( 34 | readSourceFiles, 35 | RTE.flatMap(parseFiles), 36 | RTE.tap(typeCheckExamples), 37 | RTE.flatMap(getMarkdownFiles), 38 | RTE.flatMap(writeMarkdownFiles) 39 | ) 40 | return program({ ...capabilities, config }) 41 | }) 42 | ) 43 | 44 | // ------------------------------------------------------------------------------------- 45 | // PackageJSON 46 | // ------------------------------------------------------------------------------------- 47 | 48 | interface PackageJSON { 49 | readonly name: string 50 | readonly homepage: string 51 | } 52 | 53 | const PackageJSONDecoder = pipe( 54 | Decoder.struct({ 55 | name: Decoder.string, 56 | homepage: Decoder.string 57 | }) 58 | ) 59 | 60 | const parsePackageJSON: Program = ({ fileSystem }) => { 61 | const read = pipe( 62 | fileSystem.readFile(path.join(process.cwd(), 'package.json')), 63 | TaskEither.mapLeft(() => `Unable to read package.json in "${process.cwd()}"`) 64 | ) 65 | const parse = (packageJsonSource: string) => 66 | pipe( 67 | Json.parse(packageJsonSource), 68 | Either.mapLeft((u) => Either.toError(u).message), 69 | TaskEither.fromEither 70 | ) 71 | const decode = (json: Json.Json) => 72 | pipe( 73 | PackageJSONDecoder.decode(json), 74 | Either.mapLeft((decodeError) => `Unable to decode package.json:\n${Decoder.draw(decodeError)}`), 75 | TaskEither.fromEither 76 | ) 77 | return pipe(read, TaskEither.flatMap(parse), TaskEither.flatMap(decode)) 78 | } 79 | 80 | // ------------------------------------------------------------------------------------- 81 | // config 82 | // ------------------------------------------------------------------------------------- 83 | 84 | const CONFIG_FILE_NAME = 'docs-ts.json' 85 | 86 | const getConfig = 87 | (projectName: string, projectHomepage: string): Program => 88 | (capabilities) => { 89 | const configPath = path.join(process.cwd(), CONFIG_FILE_NAME) 90 | const defaultConfig = getDefaultConfig(projectName, projectHomepage) 91 | return pipe( 92 | capabilities.fileSystem.exists(configPath), 93 | TaskEither.flatMap((exists) => 94 | exists 95 | ? pipe( 96 | capabilities.fileSystem.readFile(configPath), 97 | TaskEither.flatMap((content) => 98 | TaskEither.fromEither( 99 | pipe( 100 | Json.parse(content), 101 | Either.mapLeft((u) => Either.toError(u).message) 102 | ) 103 | ) 104 | ), 105 | TaskEither.tap(() => capabilities.logger.info(`Configuration file found`)), 106 | TaskEither.flatMap((json) => TaskEither.fromEither(Config.decode(json))), 107 | TaskEither.bimap( 108 | (decodeError) => `Invalid configuration file detected:\n${decodeError}`, 109 | (config) => ({ ...defaultConfig, ...config }) 110 | ) 111 | ) 112 | : pipe( 113 | capabilities.logger.info('No configuration file detected, using default configuration'), 114 | TaskEither.map(() => defaultConfig) 115 | ) 116 | ) 117 | ) 118 | } 119 | 120 | const getDefaultConfig = (projectName: string, projectHomepage: string): Config.Config => { 121 | return { 122 | projectName, 123 | projectHomepage, 124 | srcDir: 'src', 125 | outDir: 'docs', 126 | theme: 'pmarsceill/just-the-docs', 127 | enableSearch: true, 128 | enforceDescriptions: false, 129 | enforceExamples: false, 130 | enforceVersion: true, 131 | exclude: [], 132 | parseCompilerOptions: {}, 133 | examplesCompilerOptions: {} 134 | } 135 | } 136 | 137 | /** 138 | * @category model 139 | * @since 0.6.0 140 | */ 141 | export interface Capabilities { 142 | /** 143 | * Executes a command like: 144 | * 145 | * ```sh 146 | * ts-node examples/index.ts 147 | * ``` 148 | * 149 | * where `command = ts-node` and `executable = examples/index.ts` 150 | */ 151 | readonly spawn: (command: string, executable: string) => TaskEither.TaskEither 152 | readonly fileSystem: FileSystem 153 | readonly logger: Logger 154 | readonly addFile: (file: File) => (project: ast.Project) => void 155 | } 156 | 157 | /** 158 | * @category model 159 | * @since 0.6.0 160 | */ 161 | export interface Program
extends RTE.ReaderTaskEither {} 162 | 163 | /** 164 | * @category model 165 | * @since 0.6.0 166 | */ 167 | export interface EnvironmentWithConfig extends Capabilities { 168 | readonly config: Config.Config 169 | } 170 | 171 | /** 172 | * @category model 173 | * @since 0.6.0 174 | */ 175 | export interface ProgramWithConfig extends RTE.ReaderTaskEither {} 176 | 177 | // ------------------------------------------------------------------------------------- 178 | // filesystem APIs 179 | // ------------------------------------------------------------------------------------- 180 | 181 | const readFile = (path: string): Program => 182 | pipe( 183 | RTE.ask(), 184 | RTE.chainTaskEitherK(({ fileSystem }) => fileSystem.readFile(path)), 185 | RTE.map((content) => File(path, content, false)) 186 | ) 187 | 188 | const readFiles: (paths: ReadonlyArray) => Program> = ReadonlyArray.traverse( 189 | RTE.ApplicativePar 190 | )(readFile) 191 | 192 | const writeFile = (file: File): Program => { 193 | const overwrite: Program = pipe( 194 | RTE.ask(), 195 | RTE.chainTaskEitherK(({ fileSystem, logger }) => 196 | pipe( 197 | logger.debug(`Overwriting file ${file.path}`), 198 | TaskEither.flatMap(() => fileSystem.writeFile(file.path, file.content)) 199 | ) 200 | ) 201 | ) 202 | 203 | const skip: Program = pipe( 204 | RTE.ask(), 205 | RTE.chainTaskEitherK(({ logger }) => logger.debug(`File ${file.path} already exists, skipping creation`)) 206 | ) 207 | 208 | const write: Program = pipe( 209 | RTE.ask(), 210 | RTE.chainTaskEitherK(({ fileSystem }) => fileSystem.writeFile(file.path, file.content)) 211 | ) 212 | 213 | return pipe( 214 | RTE.ask(), 215 | RTE.flatMap(({ fileSystem }) => RTE.fromTaskEither(fileSystem.exists(file.path))), 216 | RTE.flatMap((exists) => (exists ? (file.overwrite ? overwrite : skip) : write)) 217 | ) 218 | } 219 | 220 | const writeFiles: (files: ReadonlyArray) => Program = flow( 221 | ReadonlyArray.traverse(RTE.ApplicativePar)(writeFile), 222 | RTE.map(constVoid) 223 | ) 224 | 225 | const readSourcePaths: ProgramWithConfig> = pipe( 226 | RTE.ask(), 227 | RTE.chainTaskEitherK(({ fileSystem, logger, config }) => 228 | pipe( 229 | fileSystem.search(path.join(config.srcDir, '**', '*.ts'), config.exclude), 230 | TaskEither.map(ReadonlyArray.map(path.normalize)), 231 | TaskEither.tap((paths) => pipe(logger.info(`${paths.length} module(s) found`))) 232 | ) 233 | ) 234 | ) 235 | 236 | const readSourceFiles: ProgramWithConfig> = pipe( 237 | RTE.ask(), 238 | RTE.flatMap((C) => 239 | pipe( 240 | readSourcePaths, 241 | RTE.chainTaskEitherK((paths) => pipe(C, readFiles(paths))) 242 | ) 243 | ) 244 | ) 245 | 246 | // ------------------------------------------------------------------------------------- 247 | // parsers 248 | // ------------------------------------------------------------------------------------- 249 | 250 | const parseFiles = (files: ReadonlyArray): ProgramWithConfig> => 251 | pipe( 252 | RTE.ask(), 253 | RTE.tap(({ logger }) => RTE.fromTaskEither(logger.debug('Parsing files...'))), 254 | RTE.flatMap(() => Parser.parseFiles(files)) 255 | ) 256 | 257 | // ------------------------------------------------------------------------------------- 258 | // examples 259 | // ------------------------------------------------------------------------------------- 260 | 261 | const foldFiles = Monoid.concatAll(ReadonlyArray.getMonoid()) 262 | 263 | const getExampleFiles = (modules: ReadonlyArray): ProgramWithConfig> => 264 | pipe( 265 | RTE.ask(), 266 | RTE.map((env) => 267 | pipe( 268 | modules, 269 | ReadonlyArray.flatMap((module) => { 270 | const prefix = module.path.join('-') 271 | 272 | const getDocumentableExamples = 273 | (id: string) => 274 | (documentable: Documentable): ReadonlyArray => 275 | pipe( 276 | documentable.examples, 277 | ReadonlyArray.mapWithIndex((i, content) => 278 | File( 279 | path.join(env.config.outDir, 'examples', `${prefix}-${id}-${documentable.name}-${i}.ts`), 280 | `${content}\n`, 281 | true 282 | ) 283 | ) 284 | ) 285 | 286 | const moduleExamples = getDocumentableExamples('module')(module) 287 | const methods = pipe( 288 | module.classes, 289 | ReadonlyArray.flatMap((c) => 290 | foldFiles([ 291 | pipe(c.methods, ReadonlyArray.flatMap(getDocumentableExamples(`${c.name}-method`))), 292 | pipe(c.staticMethods, ReadonlyArray.flatMap(getDocumentableExamples(`${c.name}-staticmethod`))) 293 | ]) 294 | ) 295 | ) 296 | const interfaces = pipe(module.interfaces, ReadonlyArray.flatMap(getDocumentableExamples('interface'))) 297 | const typeAliases = pipe(module.typeAliases, ReadonlyArray.flatMap(getDocumentableExamples('typealias'))) 298 | const constants = pipe(module.constants, ReadonlyArray.flatMap(getDocumentableExamples('constant'))) 299 | const functions = pipe(module.functions, ReadonlyArray.flatMap(getDocumentableExamples('function'))) 300 | 301 | return foldFiles([moduleExamples, methods, interfaces, typeAliases, constants, functions]) 302 | }) 303 | ) 304 | ) 305 | ) 306 | 307 | const addAssertImport = (code: string): string => 308 | code.indexOf('assert.') !== -1 ? `import * as assert from 'assert'\n${code}` : code 309 | 310 | const replaceProjectName = (source: string): ProgramWithConfig => 311 | pipe( 312 | RTE.ask(), 313 | RTE.map(({ config }) => { 314 | const importRegex = (projectName: string) => 315 | new RegExp(`from (?['"])${projectName}(?:/lib)?(?:/(?.*))?\\k`, 'g') 316 | 317 | return source.replace(importRegex(config.projectName), (...args) => { 318 | const groups: { path?: string } = args[args.length - 1] 319 | return `from '../../src${groups.path ? `/${groups.path}` : ''}'` 320 | }) 321 | }) 322 | ) 323 | 324 | const handleImports: (files: ReadonlyArray) => ProgramWithConfig> = ReadonlyArray.traverse( 325 | RTE.ApplicativePar 326 | )((file) => 327 | pipe( 328 | replaceProjectName(file.content), 329 | RTE.map(addAssertImport), 330 | RTE.map((content) => File(file.path, content, file.overwrite)) 331 | ) 332 | ) 333 | 334 | const getExampleIndex = (examples: ReadonlyArray): ProgramWithConfig => { 335 | const content = pipe( 336 | examples, 337 | ReadonlyArray.foldMap(S.Monoid)((example) => `import './${path.basename(example.path, '.ts')}'\n`) 338 | ) 339 | return pipe( 340 | RTE.ask(), 341 | RTE.map((env) => File(path.join(env.config.outDir, 'examples', 'index.ts'), `${content}\n`, true)) 342 | ) 343 | } 344 | 345 | const cleanExamples: ProgramWithConfig = pipe( 346 | RTE.ask(), 347 | RTE.chainTaskEitherK(({ fileSystem, config }) => fileSystem.remove(path.join(config.outDir, 'examples'))) 348 | ) 349 | 350 | const spawnTsNode: ProgramWithConfig = pipe( 351 | RTE.ask(), 352 | RTE.tap(({ logger }) => RTE.fromTaskEither(logger.debug('Type checking examples...'))), 353 | RTE.chainTaskEitherK(({ spawn, config }) => { 354 | const command = process.platform === 'win32' ? 'ts-node.cmd' : 'ts-node' 355 | const executable = path.join(process.cwd(), config.outDir, 'examples', 'index.ts') 356 | return spawn(command, executable) 357 | }) 358 | ) 359 | 360 | const writeExamples = (examples: ReadonlyArray): ProgramWithConfig => 361 | pipe( 362 | RTE.ask(), 363 | RTE.tap(({ logger }) => RTE.fromTaskEither(logger.debug('Writing examples...'))), 364 | RTE.flatMap((C) => 365 | pipe( 366 | getExampleIndex(examples), 367 | RTE.map((index) => pipe(examples, ReadonlyArray.prepend(index))), 368 | RTE.chainTaskEitherK((files) => pipe(C, writeFiles(files))) 369 | ) 370 | ) 371 | ) 372 | 373 | const writeTsConfigJson: ProgramWithConfig = pipe( 374 | RTE.ask(), 375 | RTE.tap(({ logger }) => RTE.fromTaskEither(logger.debug('Writing examples tsconfig...'))), 376 | RTE.flatMap((env) => 377 | writeFile( 378 | File( 379 | path.join(process.cwd(), env.config.outDir, 'examples', 'tsconfig.json'), 380 | JSON.stringify( 381 | { 382 | compilerOptions: env.config.examplesCompilerOptions 383 | }, 384 | null, 385 | 2 386 | ), 387 | true 388 | ) 389 | ) 390 | ) 391 | ) 392 | 393 | const typeCheckExamples = (modules: ReadonlyArray): ProgramWithConfig => 394 | pipe( 395 | getExampleFiles(modules), 396 | RTE.flatMap(handleImports), 397 | RTE.flatMap((examples) => 398 | examples.length === 0 399 | ? cleanExamples 400 | : pipe( 401 | writeExamples(examples), 402 | RTE.flatMap(() => writeTsConfigJson), 403 | RTE.flatMap(() => spawnTsNode), 404 | RTE.flatMap(() => cleanExamples) 405 | ) 406 | ) 407 | ) 408 | 409 | // ------------------------------------------------------------------------------------- 410 | // markdown 411 | // ------------------------------------------------------------------------------------- 412 | 413 | const getHome: ProgramWithConfig = pipe( 414 | RTE.ask(), 415 | RTE.map(({ config }) => 416 | File( 417 | path.join(process.cwd(), config.outDir, 'index.md'), 418 | `--- 419 | title: Home 420 | nav_order: 1 421 | --- 422 | `, 423 | false 424 | ) 425 | ) 426 | ) 427 | 428 | const getModulesIndex: ProgramWithConfig = pipe( 429 | RTE.ask(), 430 | RTE.map(({ config }) => 431 | File( 432 | path.join(process.cwd(), config.outDir, 'modules', 'index.md'), 433 | `--- 434 | title: Modules 435 | has_children: true 436 | permalink: /docs/modules 437 | nav_order: 2 438 | ---`, 439 | false 440 | ) 441 | ) 442 | ) 443 | 444 | const replace = 445 | (searchValue: string | RegExp, replaceValue: string): ((s: string) => string) => 446 | (s) => 447 | s.replace(searchValue, replaceValue) 448 | 449 | const resolveConfigYML = (previousContent: string, config: Config.Config): string => 450 | pipe( 451 | previousContent, 452 | replace(/^remote_theme:.*$/m, `remote_theme: ${config.theme}`), 453 | replace(/^search_enabled:.*$/m, `search_enabled: ${config.enableSearch}`), 454 | replace( 455 | /^ {2}'\S* on GitHub':\n {4}- '.*'/m, 456 | ` '${config.projectName} on GitHub':\n - '${config.projectHomepage}'` 457 | ) 458 | ) 459 | 460 | const getHomepageNavigationHeader = (config: Config.Config): string => { 461 | const isGitHub = config.projectHomepage.toLowerCase().includes('github') 462 | return isGitHub ? config.projectName + ' on GitHub' : 'Homepage' 463 | } 464 | 465 | const getConfigYML: ProgramWithConfig = pipe( 466 | RTE.ask(), 467 | RTE.chainTaskEitherK(({ fileSystem, config }) => { 468 | const filePath = path.join(process.cwd(), config.outDir, '_config.yml') 469 | return pipe( 470 | fileSystem.exists(filePath), 471 | TaskEither.flatMap((exists) => 472 | exists 473 | ? pipe( 474 | fileSystem.readFile(filePath), 475 | TaskEither.map((content) => File(filePath, resolveConfigYML(content, config), true)) 476 | ) 477 | : TaskEither.of( 478 | File( 479 | filePath, 480 | `remote_theme: ${config.theme} 481 | 482 | # Enable or disable the site search 483 | search_enabled: ${config.enableSearch} 484 | 485 | # Aux links for the upper right navigation 486 | aux_links: 487 | '${getHomepageNavigationHeader(config)}': 488 | - '${config.projectHomepage}'`, 489 | false 490 | ) 491 | ) 492 | ) 493 | ) 494 | }) 495 | ) 496 | 497 | const getMarkdownOutputPath = (module: Module): ProgramWithConfig => 498 | pipe( 499 | RTE.ask(), 500 | RTE.map(({ config }) => path.join(config.outDir, 'modules', `${module.path.slice(1).join(path.sep)}.md`)) 501 | ) 502 | 503 | const getModuleMarkdownFiles = (modules: ReadonlyArray): ProgramWithConfig> => 504 | pipe( 505 | modules, 506 | RTE.traverseArrayWithIndex((order, module) => 507 | pipe( 508 | getMarkdownOutputPath(module), 509 | RTE.bindTo('outputPath'), 510 | RTE.bind('content', () => RTE.right(printModule(module, order + 1))), 511 | RTE.map(({ content, outputPath }) => File(outputPath, content, true)) 512 | ) 513 | ) 514 | ) 515 | 516 | const getMarkdownFiles = (modules: ReadonlyArray): ProgramWithConfig> => 517 | pipe( 518 | RTE.sequenceArray([getHome, getModulesIndex, getConfigYML]), 519 | RTE.flatMap((meta) => 520 | pipe( 521 | getModuleMarkdownFiles(modules), 522 | RTE.map((files) => ReadonlyArray.getMonoid().concat(meta, files)) 523 | ) 524 | ) 525 | ) 526 | 527 | const writeMarkdownFiles = (files: ReadonlyArray): ProgramWithConfig => 528 | pipe( 529 | RTE.ask(), 530 | RTE.chainFirst(({ fileSystem, logger, config }) => { 531 | const outPattern = path.join(config.outDir, '**/*.ts.md') 532 | return pipe( 533 | logger.debug(`Cleaning up docs folder: deleting ${outPattern}`), 534 | TaskEither.flatMap(() => fileSystem.remove(outPattern)), 535 | RTE.fromTaskEither 536 | ) 537 | }), 538 | RTE.chainTaskEitherK((C) => 539 | pipe( 540 | C.logger.debug('Writing markdown files...'), 541 | TaskEither.flatMap(() => pipe(C, writeFiles(files))) 542 | ) 543 | ) 544 | ) 545 | -------------------------------------------------------------------------------- /src/Parser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 0.6.0 3 | */ 4 | import * as doctrine from 'doctrine' 5 | import * as Apply from 'fp-ts/Apply' 6 | import * as B from 'fp-ts/boolean' 7 | import * as E from 'fp-ts/Either' 8 | import { flow, getMonoid, pipe } from 'fp-ts/function' 9 | import * as M from 'fp-ts/Monoid' 10 | import * as O from 'fp-ts/Option' 11 | import * as Ord from 'fp-ts/Ord' 12 | import { not, Predicate } from 'fp-ts/Predicate' 13 | import * as RE from 'fp-ts/ReaderEither' 14 | import * as RTE from 'fp-ts/ReaderTaskEither' 15 | import * as RA from 'fp-ts/ReadonlyArray' 16 | import * as RNEA from 'fp-ts/ReadonlyNonEmptyArray' 17 | import * as RR from 'fp-ts/ReadonlyRecord' 18 | import * as Semigroup from 'fp-ts/Semigroup' 19 | import * as S from 'fp-ts/string' 20 | import * as T from 'fp-ts/Task' 21 | import * as Path from 'path' 22 | import * as ast from 'ts-morph' 23 | 24 | import { EnvironmentWithConfig } from './Core' 25 | import { File } from './FileSystem' 26 | import { 27 | Class, 28 | Constant, 29 | Documentable, 30 | Example, 31 | Export, 32 | Function, 33 | Interface, 34 | Method, 35 | Module, 36 | ordModule, 37 | Property, 38 | TypeAlias 39 | } from './Module' 40 | 41 | // ------------------------------------------------------------------------------------- 42 | // model 43 | // ------------------------------------------------------------------------------------- 44 | 45 | /** 46 | * @category model 47 | * @since 0.6.0 48 | */ 49 | export interface Parser extends RE.ReaderEither {} 50 | 51 | /** 52 | * @category model 53 | * @since 0.6.0 54 | */ 55 | export interface ParserEnv extends EnvironmentWithConfig { 56 | readonly path: RNEA.ReadonlyNonEmptyArray 57 | readonly sourceFile: ast.SourceFile 58 | } 59 | 60 | interface Comment { 61 | readonly description: O.Option 62 | readonly tags: RR.ReadonlyRecord>> 63 | } 64 | 65 | interface CommentInfo { 66 | readonly description: O.Option 67 | readonly since: O.Option 68 | readonly deprecated: boolean 69 | readonly examples: ReadonlyArray 70 | readonly category: O.Option 71 | } 72 | 73 | // ------------------------------------------------------------------------------------- 74 | // constructors 75 | // ------------------------------------------------------------------------------------- 76 | 77 | const CommentInfo = ( 78 | description: O.Option, 79 | since: O.Option, 80 | deprecated: boolean, 81 | examples: ReadonlyArray, 82 | category: O.Option 83 | ): CommentInfo => ({ 84 | description, 85 | since, 86 | deprecated, 87 | examples, 88 | category 89 | }) 90 | 91 | // ------------------------------------------------------------------------------------- 92 | // parsers 93 | // ------------------------------------------------------------------------------------- 94 | 95 | // ------------------------------------------------------------------------------------- 96 | // utils 97 | // ------------------------------------------------------------------------------------- 98 | 99 | const semigroupError = pipe(S.Semigroup, Semigroup.intercalate('\n')) 100 | 101 | const applicativeParser = RE.getApplicativeReaderValidation(semigroupError) 102 | 103 | const sequenceS = Apply.sequenceS(applicativeParser) 104 | 105 | const traverse = RA.traverse(applicativeParser) 106 | 107 | const every = (predicates: ReadonlyArray>): ((a: A) => boolean) => 108 | M.concatAll(getMonoid(B.MonoidAll)())(predicates) 109 | 110 | const some = (predicates: ReadonlyArray>): ((a: A) => boolean) => 111 | M.concatAll(getMonoid(B.MonoidAny)())(predicates) 112 | 113 | const ordByName = pipe( 114 | S.Ord, 115 | Ord.contramap(({ name }: { name: string }) => name) 116 | ) 117 | 118 | const sortModules = RA.sort(ordModule) 119 | 120 | const isNonEmptyString = (s: string) => s.length > 0 121 | 122 | /** 123 | * @internal 124 | */ 125 | export const stripImportTypes = (s: string): string => s.replace(/import\("((?!").)*"\)./g, '') 126 | 127 | const getJSDocText: (jsdocs: ReadonlyArray) => string = RA.foldRight( 128 | () => '', 129 | (_, last) => last.getText() 130 | ) 131 | 132 | const shouldIgnore: Predicate = some([ 133 | (comment) => pipe(comment.tags, RR.lookup('internal'), O.isSome), 134 | (comment) => pipe(comment.tags, RR.lookup('ignore'), O.isSome) 135 | ]) 136 | 137 | const isVariableDeclarationList = ( 138 | u: ast.VariableDeclarationList | ast.CatchClause 139 | ): u is ast.VariableDeclarationList => u.getKind() === ast.ts.SyntaxKind.VariableDeclarationList 140 | 141 | const isVariableStatement = ( 142 | u: ast.VariableStatement | ast.ForStatement | ast.ForOfStatement | ast.ForInStatement 143 | ): u is ast.VariableStatement => u.getKind() === ast.ts.SyntaxKind.VariableStatement 144 | 145 | // ------------------------------------------------------------------------------------- 146 | // comments 147 | // ------------------------------------------------------------------------------------- 148 | 149 | const getSinceTag = (name: string, comment: Comment): Parser> => 150 | pipe( 151 | RE.ask(), 152 | RE.chainEitherK((env) => 153 | pipe( 154 | comment.tags, 155 | RR.lookup('since'), 156 | O.flatMap(RNEA.head), 157 | O.fold( 158 | () => 159 | env.config.enforceVersion 160 | ? E.left(`Missing @since tag in ${env.path.join('/')}#${name} documentation`) 161 | : E.right(O.none), 162 | (since) => E.right(O.some(since)) 163 | ) 164 | ) 165 | ) 166 | ) 167 | 168 | const getCategoryTag = (name: string, comment: Comment): Parser> => 169 | pipe( 170 | RE.ask(), 171 | RE.chainEitherK((env) => 172 | pipe( 173 | comment.tags, 174 | RR.lookup('category'), 175 | O.flatMap(RNEA.head), 176 | E.fromPredicate( 177 | not(every([O.isNone, () => RR.has('category', comment.tags)])), 178 | () => `Missing @category value in ${env.path.join('/')}#${name} documentation` 179 | ) 180 | ) 181 | ) 182 | ) 183 | 184 | const getDescription = (name: string, comment: Comment): Parser> => 185 | pipe( 186 | RE.ask(), 187 | RE.chainEitherK((env) => 188 | pipe( 189 | comment.description, 190 | O.fold( 191 | () => 192 | env.config.enforceDescriptions 193 | ? E.left(`Missing description in ${env.path.join('/')}#${name} documentation`) 194 | : E.right(O.none), 195 | (description) => E.right(O.some(description)) 196 | ) 197 | ) 198 | ) 199 | ) 200 | 201 | const getExamples = (name: string, comment: Comment, isModule: boolean): Parser> => 202 | pipe( 203 | RE.ask(), 204 | RE.chainEitherK((env) => 205 | pipe( 206 | comment.tags, 207 | RR.lookup('example'), 208 | O.map(RA.compact), 209 | O.fold( 210 | () => 211 | M.concatAll(B.MonoidAll)([env.config.enforceExamples, !isModule]) 212 | ? E.left(`Missing examples in ${env.path.join('/')}#${name} documentation`) 213 | : E.right>(RA.empty), 214 | (examples) => 215 | M.concatAll(B.MonoidAll)([env.config.enforceExamples, RA.isEmpty(examples), !isModule]) 216 | ? E.left(`Missing examples in ${env.path.join('/')}#${name} documentation`) 217 | : E.right(examples) 218 | ) 219 | ) 220 | ) 221 | ) 222 | 223 | /** 224 | * @internal 225 | */ 226 | export const getCommentInfo = 227 | (name: string, isModule = false) => 228 | (text: string): Parser => 229 | pipe( 230 | RE.right(parseComment(text)), 231 | RE.bindTo('comment'), 232 | RE.bind('since', ({ comment }) => getSinceTag(name, comment)), 233 | RE.bind('category', ({ comment }) => getCategoryTag(name, comment)), 234 | RE.bind('description', ({ comment }) => getDescription(name, comment)), 235 | RE.bind('examples', ({ comment }) => getExamples(name, comment, isModule)), 236 | RE.bind('deprecated', ({ comment }) => RE.right(pipe(comment.tags, RR.lookup('deprecated'), O.isSome))), 237 | RE.map(({ category, deprecated, description, examples, since }) => { 238 | return CommentInfo(description, since, deprecated, examples, category) 239 | }) 240 | ) 241 | 242 | /** 243 | * @internal 244 | */ 245 | export const parseComment = (text: string): Comment => { 246 | const annotation: doctrine.Annotation = doctrine.parse(text, { unwrap: true }) 247 | const tags = pipe( 248 | annotation.tags, 249 | RNEA.groupBy((tag) => tag.title), 250 | RR.map(RNEA.map((tag) => pipe(O.fromNullable(tag.description), O.filter(isNonEmptyString)))) 251 | ) 252 | const description = pipe(O.fromNullable(annotation.description), O.filter(isNonEmptyString)) 253 | return { description, tags } 254 | } 255 | 256 | // ------------------------------------------------------------------------------------- 257 | // interfaces 258 | // ------------------------------------------------------------------------------------- 259 | 260 | const parseInterfaceDeclaration = (id: ast.InterfaceDeclaration): Parser => 261 | pipe( 262 | getJSDocText(id.getJsDocs()), 263 | getCommentInfo(id.getName()), 264 | RE.map((info) => 265 | Interface( 266 | Documentable(id.getName(), info.description, info.since, info.deprecated, info.examples, info.category), 267 | id.getText() 268 | ) 269 | ) 270 | ) 271 | 272 | /** 273 | * @category parsers 274 | * @since 0.6.0 275 | */ 276 | export const parseInterfaces: Parser> = pipe( 277 | RE.asks, string>((env) => 278 | pipe( 279 | env.sourceFile.getInterfaces(), 280 | RA.filter( 281 | every([ 282 | (id) => id.isExported(), 283 | (id) => pipe(id.getJsDocs(), not(flow(getJSDocText, parseComment, shouldIgnore))) 284 | ]) 285 | ) 286 | ) 287 | ), 288 | RE.flatMap(flow(traverse(parseInterfaceDeclaration), RE.map(RA.sort(ordByName)))) 289 | ) 290 | 291 | // ------------------------------------------------------------------------------------- 292 | // functions 293 | // ------------------------------------------------------------------------------------- 294 | 295 | const getFunctionDeclarationSignature = (f: ast.FunctionDeclaration): string => { 296 | const text = f.getText() 297 | return pipe( 298 | O.fromNullable(f.compilerNode.body), 299 | O.fold( 300 | () => text.replace('export function ', 'export declare function '), 301 | (body) => { 302 | const end = body.getStart() - f.getStart() - 1 303 | return text.substring(0, end).replace('export function ', 'export declare function ') 304 | } 305 | ) 306 | ) 307 | } 308 | 309 | const getFunctionDeclarationJSDocs = (fd: ast.FunctionDeclaration): ReadonlyArray => 310 | pipe( 311 | fd.getOverloads(), 312 | RA.foldLeft( 313 | () => fd.getJsDocs(), 314 | (firstOverload) => firstOverload.getJsDocs() 315 | ) 316 | ) 317 | 318 | const parseFunctionDeclaration = (fd: ast.FunctionDeclaration): Parser => 319 | pipe( 320 | RE.ask(), 321 | RE.chain((env) => 322 | pipe( 323 | O.fromNullable(fd.getName()), 324 | O.flatMap(O.fromPredicate((name) => name.length > 0)), 325 | RE.fromOption(() => `Missing function name in module ${env.path.join('/')}`) 326 | ) 327 | ), 328 | RE.flatMap((name) => 329 | pipe( 330 | getJSDocText(getFunctionDeclarationJSDocs(fd)), 331 | getCommentInfo(name), 332 | RE.map((info) => { 333 | const signatures = pipe( 334 | fd.getOverloads(), 335 | RA.foldRight( 336 | () => RA.of(getFunctionDeclarationSignature(fd)), 337 | (init, last) => 338 | pipe(init.map(getFunctionDeclarationSignature), RA.append(getFunctionDeclarationSignature(last))) 339 | ) 340 | ) 341 | return Function( 342 | Documentable(name, info.description, info.since, info.deprecated, info.examples, info.category), 343 | signatures 344 | ) 345 | }) 346 | ) 347 | ) 348 | ) 349 | 350 | const parseFunctionVariableDeclaration = (vd: ast.VariableDeclaration): Parser => { 351 | const vs: any = vd.getParent().getParent() 352 | const name = vd.getName() 353 | return pipe( 354 | getJSDocText(vs.getJsDocs()), 355 | getCommentInfo(name), 356 | RE.map((info) => { 357 | const signature = `export declare const ${name}: ${stripImportTypes(vd.getType().getText(vd))}` 358 | return Function( 359 | Documentable(name, info.description, info.since, info.deprecated, info.examples, info.category), 360 | RA.of(signature) 361 | ) 362 | }) 363 | ) 364 | } 365 | 366 | const getFunctionDeclarations: RE.ReaderEither< 367 | ParserEnv, 368 | string, 369 | { 370 | functions: ReadonlyArray 371 | arrows: ReadonlyArray 372 | } 373 | > = RE.asks((env) => ({ 374 | functions: pipe( 375 | env.sourceFile.getFunctions(), 376 | RA.filter( 377 | every([ 378 | (fd) => fd.isExported(), 379 | not(flow(getFunctionDeclarationJSDocs, getJSDocText, parseComment, shouldIgnore)) 380 | ]) 381 | ) 382 | ), 383 | arrows: pipe( 384 | env.sourceFile.getVariableDeclarations(), 385 | RA.filter( 386 | every([ 387 | (vd) => isVariableDeclarationList(vd.getParent()), 388 | (vd) => isVariableStatement(vd.getParent().getParent() as any), 389 | (vd) => 390 | pipe( 391 | vd.getInitializer(), 392 | every([ 393 | flow(O.fromNullable, O.flatMap(O.fromPredicate(ast.Node.isFunctionLikeDeclaration)), O.isSome), 394 | () => 395 | pipe( 396 | (vd.getParent().getParent() as ast.VariableStatement).getJsDocs(), 397 | not(flow(getJSDocText, parseComment, shouldIgnore)) 398 | ), 399 | () => (vd.getParent().getParent() as ast.VariableStatement).isExported() 400 | ]) 401 | ) 402 | ]) 403 | ) 404 | ) 405 | })) 406 | 407 | /** 408 | * @category parsers 409 | * @since 0.6.0 410 | */ 411 | export const parseFunctions: Parser> = pipe( 412 | getFunctionDeclarations, 413 | RE.flatMap(({ arrows, functions }) => 414 | sequenceS({ 415 | functionDeclarations: pipe(functions, traverse(parseFunctionDeclaration)), 416 | variableDeclarations: pipe(arrows, traverse(parseFunctionVariableDeclaration)) 417 | }) 418 | ), 419 | RE.map(({ functionDeclarations, variableDeclarations }) => 420 | RA.getMonoid().concat(functionDeclarations, variableDeclarations) 421 | ) 422 | ) 423 | 424 | // ------------------------------------------------------------------------------------- 425 | // type aliases 426 | // ------------------------------------------------------------------------------------- 427 | 428 | const parseTypeAliasDeclaration = (ta: ast.TypeAliasDeclaration): Parser => 429 | pipe( 430 | RE.of(ta.getName()), 431 | RE.flatMap((name) => 432 | pipe( 433 | getJSDocText(ta.getJsDocs()), 434 | getCommentInfo(name), 435 | RE.map((info) => 436 | TypeAlias( 437 | Documentable(name, info.description, info.since, info.deprecated, info.examples, info.category), 438 | ta.getText() 439 | ) 440 | ) 441 | ) 442 | ) 443 | ) 444 | 445 | /** 446 | * @category parsers 447 | * @since 0.6.0 448 | */ 449 | export const parseTypeAliases: Parser> = pipe( 450 | RE.asks((env: ParserEnv) => 451 | pipe( 452 | env.sourceFile.getTypeAliases(), 453 | RA.filter( 454 | every([ 455 | (alias) => alias.isExported(), 456 | (alias) => pipe(alias.getJsDocs(), not(flow(getJSDocText, parseComment, shouldIgnore))) 457 | ]) 458 | ) 459 | ) 460 | ), 461 | RE.flatMap(traverse(parseTypeAliasDeclaration)), 462 | RE.map(RA.sort(ordByName)) 463 | ) 464 | 465 | // ------------------------------------------------------------------------------------- 466 | // constants 467 | // ------------------------------------------------------------------------------------- 468 | 469 | const parseConstantVariableDeclaration = (vd: ast.VariableDeclaration): Parser => { 470 | const vs: any = vd.getParent().getParent() 471 | const name = vd.getName() 472 | return pipe( 473 | getJSDocText(vs.getJsDocs()), 474 | getCommentInfo(name), 475 | RE.map((info) => { 476 | const type = stripImportTypes(vd.getType().getText(vd)) 477 | const signature = `export declare const ${name}: ${type}` 478 | return Constant( 479 | Documentable(name, info.description, info.since, info.deprecated, info.examples, info.category), 480 | signature 481 | ) 482 | }) 483 | ) 484 | } 485 | 486 | /** 487 | * @category parsers 488 | * @since 0.6.0 489 | */ 490 | export const parseConstants: Parser> = pipe( 491 | RE.asks((env: ParserEnv) => 492 | pipe( 493 | env.sourceFile.getVariableDeclarations(), 494 | RA.filter( 495 | every([ 496 | (vd) => isVariableDeclarationList(vd.getParent()), 497 | (vd) => isVariableStatement(vd.getParent().getParent() as any), 498 | (vd) => 499 | pipe( 500 | vd.getInitializer(), 501 | every([ 502 | flow(O.fromNullable, O.flatMap(O.fromPredicate(not(ast.Node.isFunctionLikeDeclaration))), O.isSome), 503 | () => 504 | pipe( 505 | (vd.getParent().getParent() as ast.VariableStatement).getJsDocs(), 506 | not(flow(getJSDocText, parseComment, shouldIgnore)) 507 | ), 508 | () => (vd.getParent().getParent() as ast.VariableStatement).isExported() 509 | ]) 510 | ) 511 | ]) 512 | ) 513 | ) 514 | ), 515 | RE.flatMap(traverse(parseConstantVariableDeclaration)) 516 | ) 517 | 518 | // ------------------------------------------------------------------------------------- 519 | // exports 520 | // ------------------------------------------------------------------------------------- 521 | 522 | const parseExportSpecifier = (es: ast.ExportSpecifier): Parser => 523 | pipe( 524 | RE.ask(), 525 | RE.flatMap((env) => 526 | pipe( 527 | RE.of(es.compilerNode.name.text), 528 | RE.bindTo('name'), 529 | RE.bind('type', () => RE.of(stripImportTypes(es.getType().getText(es)))), 530 | RE.bind('signature', ({ name, type }) => RE.of(`export declare const ${name}: ${type}`)), 531 | RE.flatMap(({ name, signature }) => 532 | pipe( 533 | es.getLeadingCommentRanges(), 534 | RA.head, 535 | RE.fromOption(() => `Missing ${name} documentation in ${env.path.join('/')}`), 536 | RE.flatMap((commentRange) => pipe(commentRange.getText(), getCommentInfo(name))), 537 | RE.map((info) => 538 | Export( 539 | Documentable(name, info.description, info.since, info.deprecated, info.examples, info.category), 540 | signature 541 | ) 542 | ) 543 | ) 544 | ) 545 | ) 546 | ) 547 | ) 548 | 549 | const parseExportDeclaration = (ed: ast.ExportDeclaration): Parser> => 550 | pipe(ed.getNamedExports(), traverse(parseExportSpecifier)) 551 | 552 | /** 553 | * @category parsers 554 | * @since 0.6.0 555 | */ 556 | export const parseExports: Parser> = pipe( 557 | RE.asks((env: ParserEnv) => env.sourceFile.getExportDeclarations()), 558 | RE.flatMap(traverse(parseExportDeclaration)), 559 | RE.map(RA.flatten) 560 | ) 561 | 562 | // ------------------------------------------------------------------------------------- 563 | // classes 564 | // ------------------------------------------------------------------------------------- 565 | 566 | const getTypeParameters = (tps: ReadonlyArray): string => 567 | tps.length === 0 ? '' : `<${tps.map((p) => p.getName()).join(', ')}>` 568 | 569 | const getMethodSignature = (md: ast.MethodDeclaration): string => 570 | pipe( 571 | O.fromNullable(md.compilerNode.body), 572 | O.fold( 573 | () => md.getText(), 574 | (body) => { 575 | const end = body.getStart() - md.getStart() - 1 576 | return md.getText().substring(0, end) 577 | } 578 | ) 579 | ) 580 | 581 | const parseMethod = (md: ast.MethodDeclaration): Parser> => 582 | pipe( 583 | RE.of(md.getName()), 584 | RE.bindTo('name'), 585 | RE.bind('overloads', () => RE.of(md.getOverloads())), 586 | RE.bind('jsdocs', ({ overloads }) => 587 | RE.of( 588 | pipe( 589 | overloads, 590 | RA.foldLeft( 591 | () => md.getJsDocs(), 592 | (x) => x.getJsDocs() 593 | ) 594 | ) 595 | ) 596 | ), 597 | RE.flatMap(({ jsdocs, overloads, name }) => 598 | shouldIgnore(parseComment(getJSDocText(jsdocs))) 599 | ? RE.right(O.none) 600 | : pipe( 601 | getJSDocText(jsdocs), 602 | getCommentInfo(name), 603 | RE.map((info) => { 604 | const signatures = pipe( 605 | overloads, 606 | RA.foldRight( 607 | () => RA.of(getMethodSignature(md)), 608 | (init, last) => 609 | pipe( 610 | init.map((md) => md.getText()), 611 | RA.append(getMethodSignature(last)) 612 | ) 613 | ) 614 | ) 615 | return O.some( 616 | Method( 617 | Documentable(name, info.description, info.since, info.deprecated, info.examples, info.category), 618 | signatures 619 | ) 620 | ) 621 | }) 622 | ) 623 | ) 624 | ) 625 | 626 | const parseProperty = 627 | (classname: string) => 628 | (pd: ast.PropertyDeclaration): Parser => { 629 | const name = pd.getName() 630 | return pipe( 631 | getJSDocText(pd.getJsDocs()), 632 | getCommentInfo(`${classname}#${name}`), 633 | RE.map((info) => { 634 | const type = stripImportTypes(pd.getType().getText(pd)) 635 | const readonly = pipe( 636 | O.fromNullable(pd.getFirstModifierByKind(ast.ts.SyntaxKind.ReadonlyKeyword)), 637 | O.fold( 638 | () => '', 639 | () => 'readonly ' 640 | ) 641 | ) 642 | const signature = `${readonly}${name}: ${type}` 643 | return Property( 644 | Documentable(name, info.description, info.since, info.deprecated, info.examples, info.category), 645 | signature 646 | ) 647 | }) 648 | ) 649 | } 650 | 651 | const parseProperties = (name: string, c: ast.ClassDeclaration): Parser> => 652 | pipe( 653 | c.getProperties(), 654 | // take public, instance properties 655 | RA.filter( 656 | every([ 657 | (prop) => !prop.isStatic(), 658 | (prop) => pipe(prop.getFirstModifierByKind(ast.ts.SyntaxKind.PrivateKeyword), O.fromNullable, O.isNone), 659 | (prop) => pipe(prop.getJsDocs(), not(flow(getJSDocText, parseComment, shouldIgnore))) 660 | ]) 661 | ), 662 | traverse(parseProperty(name)) 663 | ) 664 | 665 | /** 666 | * @internal 667 | */ 668 | export const getConstructorDeclarationSignature = (c: ast.ConstructorDeclaration): string => 669 | pipe( 670 | O.fromNullable(c.compilerNode.body), 671 | O.fold( 672 | () => c.getText(), 673 | (body) => { 674 | const end = body.getStart() - c.getStart() - 1 675 | return c.getText().substring(0, end) 676 | } 677 | ) 678 | ) 679 | 680 | const getClassName = (c: ast.ClassDeclaration): Parser => 681 | pipe( 682 | RE.ask(), 683 | RE.chain((env) => 684 | pipe( 685 | O.fromNullable(c.getName()), 686 | RE.fromOption(() => `Missing class name in module ${env.path.join('/')}`) 687 | ) 688 | ) 689 | ) 690 | 691 | const getClassCommentInfo = (name: string, c: ast.ClassDeclaration): Parser => 692 | pipe(c.getJsDocs(), getJSDocText, getCommentInfo(name)) 693 | 694 | const getClassDeclarationSignature = (name: string, c: ast.ClassDeclaration): Parser => 695 | pipe( 696 | RE.ask(), 697 | RE.map(() => getTypeParameters(c.getTypeParameters())), 698 | RE.map((typeParameters) => 699 | pipe( 700 | c.getConstructors(), 701 | RA.foldLeft( 702 | () => `export declare class ${name}${typeParameters}`, 703 | (head) => `export declare class ${name}${typeParameters} { ${getConstructorDeclarationSignature(head)} }` 704 | ) 705 | ) 706 | ) 707 | ) 708 | 709 | const parseClass = (c: ast.ClassDeclaration): Parser => 710 | pipe( 711 | getClassName(c), 712 | RE.bindTo('name'), 713 | RE.bind('info', ({ name }) => getClassCommentInfo(name, c)), 714 | RE.bind('signature', ({ name }) => getClassDeclarationSignature(name, c)), 715 | RE.bind('methods', () => pipe(c.getInstanceMethods(), traverse(parseMethod), RE.map(RA.compact))), 716 | RE.bind('staticMethods', () => pipe(c.getStaticMethods(), traverse(parseMethod), RE.map(RA.compact))), 717 | RE.bind('properties', ({ name }) => parseProperties(name, c)), 718 | RE.map(({ methods, staticMethods, properties, info, name, signature }) => 719 | Class( 720 | Documentable(name, info.description, info.since, info.deprecated, info.examples, info.category), 721 | signature, 722 | methods, 723 | staticMethods, 724 | properties 725 | ) 726 | ) 727 | ) 728 | 729 | const getClasses: Parser> = RE.asks((env: ParserEnv) => 730 | pipe( 731 | env.sourceFile.getClasses(), 732 | RA.filter((c) => c.isExported()) 733 | ) 734 | ) 735 | 736 | /** 737 | * @category parsers 738 | * @since 0.6.0 739 | */ 740 | export const parseClasses: Parser> = pipe( 741 | getClasses, 742 | RE.flatMap(traverse(parseClass)), 743 | RE.map(RA.sort(ordByName)) 744 | ) 745 | 746 | // ------------------------------------------------------------------------------------- 747 | // modules 748 | // ------------------------------------------------------------------------------------- 749 | 750 | const getModuleName = (path: RNEA.ReadonlyNonEmptyArray): string => Path.parse(RNEA.last(path)).name 751 | 752 | /** 753 | * @internal 754 | */ 755 | export const parseModuleDocumentation: Parser = pipe( 756 | RE.ask(), 757 | RE.chainEitherK((env) => { 758 | const name = getModuleName(env.path) 759 | // if any of the settings enforcing documentation are set to `true`, then 760 | // a module should have associated documentation 761 | const isDocumentationRequired = M.concatAll(B.MonoidAny)([ 762 | env.config.enforceDescriptions, 763 | env.config.enforceVersion 764 | ]) 765 | const onMissingDocumentation = () => 766 | isDocumentationRequired 767 | ? E.left(`Missing documentation in ${env.path.join('/')} module`) 768 | : E.right(Documentable(name, O.none, O.none, false, RA.empty, O.none)) 769 | return pipe( 770 | env.sourceFile.getStatements(), 771 | RA.foldLeft(onMissingDocumentation, (statement) => 772 | pipe( 773 | statement.getLeadingCommentRanges(), 774 | RA.foldLeft(onMissingDocumentation, (commentRange) => 775 | pipe( 776 | getCommentInfo(name, true)(commentRange.getText())(env), 777 | E.map((info) => 778 | Documentable(name, info.description, info.since, info.deprecated, info.examples, info.category) 779 | ) 780 | ) 781 | ) 782 | ) 783 | ) 784 | ) 785 | }) 786 | ) 787 | 788 | /** 789 | * @category parsers 790 | * @since 0.6.0 791 | */ 792 | export const parseModule: Parser = pipe( 793 | RE.ask(), 794 | RE.flatMap((env) => 795 | pipe( 796 | parseModuleDocumentation, 797 | RE.bindTo('documentation'), 798 | RE.bind('interfaces', () => parseInterfaces), 799 | RE.bind('functions', () => parseFunctions), 800 | RE.bind('typeAliases', () => parseTypeAliases), 801 | RE.bind('classes', () => parseClasses), 802 | RE.bind('constants', () => parseConstants), 803 | RE.bind('exports', () => parseExports), 804 | RE.map(({ documentation, classes, interfaces, functions, typeAliases, constants, exports }) => 805 | Module(documentation, env.path, classes, interfaces, functions, typeAliases, constants, exports) 806 | ) 807 | ) 808 | ) 809 | ) 810 | 811 | // ------------------------------------------------------------------------------------- 812 | // files 813 | // ------------------------------------------------------------------------------------- 814 | 815 | /** 816 | * @internal 817 | */ 818 | export const parseFile = 819 | (project: ast.Project) => 820 | (file: File): RTE.ReaderTaskEither => 821 | pipe( 822 | RTE.ask(), 823 | RTE.flatMap((env) => 824 | pipe( 825 | RTE.right>( 826 | file.path.split(Path.sep) as any 827 | ), 828 | RTE.bindTo('path'), 829 | RTE.bind( 830 | 'sourceFile', 831 | (): RTE.ReaderTaskEither => 832 | pipe( 833 | O.fromNullable(project.getSourceFile(file.path)), 834 | RTE.fromOption(() => `Unable to locate file: ${file.path}`) 835 | ) 836 | ), 837 | RTE.chainEitherK((menv) => parseModule({ ...env, ...menv })) 838 | ) 839 | ) 840 | ) 841 | 842 | const createProject = (files: ReadonlyArray): RTE.ReaderTaskEither => 843 | pipe( 844 | RTE.ask(), 845 | RTE.flatMap((env) => { 846 | const options: ast.ProjectOptions = { 847 | compilerOptions: { 848 | strict: true, 849 | ...env.config.parseCompilerOptions 850 | } 851 | } 852 | const project = new ast.Project(options) 853 | pipe( 854 | files, 855 | RA.map((file) => env.addFile(file)(project)) 856 | ) 857 | return RTE.of(project) 858 | }) 859 | ) 860 | 861 | /** 862 | * @category parsers 863 | * @since 0.6.0 864 | */ 865 | export const parseFiles = ( 866 | files: ReadonlyArray 867 | ): RTE.ReaderTaskEither> => 868 | pipe( 869 | createProject(files), 870 | RTE.flatMap((project) => 871 | pipe(files, RA.traverse(RTE.getApplicativeReaderTaskValidation(T.ApplyPar, semigroupError))(parseFile(project))) 872 | ), 873 | RTE.map( 874 | flow( 875 | RA.filter((module) => !module.deprecated), 876 | sortModules 877 | ) 878 | ) 879 | ) 880 | -------------------------------------------------------------------------------- /test/Parser.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import * as E from 'fp-ts/Either' 3 | import { pipe } from 'fp-ts/function' 4 | import * as O from 'fp-ts/lib/Option' 5 | import * as RA from 'fp-ts/ReadonlyArray' 6 | import * as ast from 'ts-morph' 7 | 8 | import * as Config from '../src/Config' 9 | import * as FS from '../src/FileSystem' 10 | import * as L from '../src/Logger' 11 | import * as _ from '../src/Parser' 12 | import { spawn } from '../src/Spawn' 13 | import { assertLeft, assertRight } from './util' 14 | 15 | let testCounter = 0 16 | 17 | const project = new ast.Project({ 18 | compilerOptions: { strict: true }, 19 | useInMemoryFileSystem: true 20 | }) 21 | 22 | const addFileToProject = (file: FS.File) => (project: ast.Project) => 23 | project.createSourceFile(file.path, file.content, { overwrite: file.overwrite }) 24 | 25 | const config: Config.Config = { 26 | projectName: 'docs-ts', 27 | projectHomepage: 'https://github.com/gcanti/docs-ts', 28 | srcDir: 'src', 29 | outDir: 'docs', 30 | theme: 'pmarsceill/just-the-docs', 31 | enableSearch: true, 32 | enforceDescriptions: false, 33 | enforceExamples: false, 34 | enforceVersion: true, 35 | exclude: RA.empty, 36 | parseCompilerOptions: {}, 37 | examplesCompilerOptions: {} 38 | } 39 | 40 | const getTestEnv = (sourceText: string): _.ParserEnv => ({ 41 | path: ['test'], 42 | sourceFile: project.createSourceFile(`test-${testCounter++}.ts`, sourceText), 43 | spawn: spawn, 44 | fileSystem: FS.FileSystem, 45 | logger: L.Logger, 46 | config, 47 | addFile: addFileToProject 48 | }) 49 | 50 | describe.concurrent('Parser', () => { 51 | describe.concurrent('parsers', () => { 52 | describe.concurrent('parseInterfaces', () => { 53 | it('should return no `Interface`s if the file is empty', () => { 54 | const env = getTestEnv('') 55 | 56 | assert.deepStrictEqual(_.parseInterfaces(env), E.right(RA.empty)) 57 | }) 58 | 59 | it('should return no `Interface`s if there are no exported interfaces', () => { 60 | const env = getTestEnv('interface A {}') 61 | 62 | assert.deepStrictEqual(_.parseInterfaces(env), E.right(RA.empty)) 63 | }) 64 | 65 | it('should return an `Interface`', () => { 66 | const env = getTestEnv( 67 | `/** 68 | * a description... 69 | * @since 1.0.0 70 | * @deprecated 71 | */ 72 | export interface A {}` 73 | ) 74 | assert.deepStrictEqual( 75 | _.parseInterfaces(env), 76 | E.right([ 77 | { 78 | _tag: 'Interface', 79 | deprecated: true, 80 | description: O.some('a description...'), 81 | name: 'A', 82 | signature: 'export interface A {}', 83 | since: O.some('1.0.0'), 84 | examples: RA.empty, 85 | category: O.none 86 | } 87 | ]) 88 | ) 89 | }) 90 | 91 | it('should return interfaces sorted by name', () => { 92 | const env = getTestEnv( 93 | ` 94 | /** 95 | * @since 1.0.0 96 | */ 97 | export interface B {} 98 | /** 99 | * @since 1.0.0 100 | */ 101 | export interface A {} 102 | ` 103 | ) 104 | assert.deepStrictEqual( 105 | _.parseInterfaces(env), 106 | E.right([ 107 | { 108 | _tag: 'Interface', 109 | name: 'A', 110 | description: O.none, 111 | since: O.some('1.0.0'), 112 | deprecated: false, 113 | category: O.none, 114 | examples: RA.empty, 115 | signature: 'export interface A {}' 116 | }, 117 | { 118 | _tag: 'Interface', 119 | name: 'B', 120 | description: O.none, 121 | since: O.some('1.0.0'), 122 | deprecated: false, 123 | category: O.none, 124 | examples: RA.empty, 125 | signature: 'export interface B {}' 126 | } 127 | ]) 128 | ) 129 | }) 130 | }) 131 | 132 | describe.concurrent('parseFunctions', () => { 133 | it('should raise an error if the function is anonymous', () => { 134 | const env = getTestEnv(`export function(a: number, b: number): number { return a + b }`) 135 | const expected = 'Missing function name in module test' 136 | 137 | assertLeft(pipe(env, _.parseFunctions), (error) => assert.strictEqual(error, expected)) 138 | }) 139 | 140 | it('should not return private function declarations', () => { 141 | const env = getTestEnv(`function sum(a: number, b: number): number { return a + b }`) 142 | 143 | assert.deepStrictEqual(_.parseFunctions(env), E.right(RA.empty)) 144 | }) 145 | 146 | it('should not return ignored function declarations', () => { 147 | const env = getTestEnv( 148 | `/** 149 | * @ignore 150 | */ 151 | export function sum(a: number, b: number): number { return a + b }` 152 | ) 153 | 154 | assert.deepStrictEqual(_.parseFunctions(env), E.right(RA.empty)) 155 | }) 156 | 157 | it('should not return ignored function declarations with overloads', () => { 158 | const env = getTestEnv( 159 | `/** 160 | * @ignore 161 | */ 162 | export function sum(a: number, b: number) 163 | export function sum(a: number, b: number): number { return a + b }` 164 | ) 165 | 166 | assert.deepStrictEqual(_.parseFunctions(env), E.right(RA.empty)) 167 | }) 168 | 169 | it('should not return internal function declarations', () => { 170 | const env = getTestEnv( 171 | `/** 172 | * @internal 173 | */ 174 | export function sum(a: number, b: number): number { return a + b }` 175 | ) 176 | 177 | assert.deepStrictEqual(_.parseFunctions(env), E.right(RA.empty)) 178 | }) 179 | 180 | it('should not return internal function declarations even with overloads', () => { 181 | const env = getTestEnv( 182 | `/** 183 | * @internal 184 | */ 185 | export function sum(a: number, b: number) 186 | export function sum(a: number, b: number): number { return a + b }` 187 | ) 188 | 189 | assert.deepStrictEqual(_.parseFunctions(env), E.right(RA.empty)) 190 | }) 191 | 192 | it('should not return private const function declarations', () => { 193 | const env = getTestEnv(`const sum = (a: number, b: number): number => a + b `) 194 | 195 | assertRight(pipe(env, _.parseFunctions), (actual) => assert.deepStrictEqual(actual, RA.empty)) 196 | }) 197 | 198 | it('should not return internal const function declarations', () => { 199 | const env = getTestEnv( 200 | `/** 201 | * @internal 202 | */ 203 | export const sum = (a: number, b: number): number => a + b ` 204 | ) 205 | 206 | assert.deepStrictEqual(_.parseFunctions(env), E.right(RA.empty)) 207 | }) 208 | 209 | it('should account for nullable polymorphic return types', () => { 210 | const env = getTestEnv( 211 | `/** 212 | * @since 1.0.0 213 | */ 214 | export const toNullable = (ma: A | null): A | null => ma` 215 | ) 216 | 217 | assert.deepStrictEqual( 218 | _.parseFunctions(env), 219 | E.right([ 220 | { 221 | _tag: 'Function', 222 | deprecated: false, 223 | description: O.none, 224 | name: 'toNullable', 225 | signatures: ['export declare const toNullable: (ma: A | null) => A | null'], 226 | since: O.some('1.0.0'), 227 | examples: [], 228 | category: O.none 229 | } 230 | ]) 231 | ) 232 | }) 233 | 234 | it('should return a const function declaration', () => { 235 | const env = getTestEnv( 236 | `/** 237 | * a description... 238 | * @since 1.0.0 239 | * @example 240 | * assert.deepStrictEqual(f(1, 2), { a: 1, b: 2 }) 241 | * @example 242 | * assert.deepStrictEqual(f(3, 4), { a: 3, b: 4 }) 243 | * @deprecated 244 | */ 245 | export const f = (a: number, b: number): { [key: string]: number } => ({ a, b })` 246 | ) 247 | 248 | assert.deepStrictEqual( 249 | _.parseFunctions(env), 250 | E.right([ 251 | { 252 | _tag: 'Function', 253 | deprecated: true, 254 | description: O.some('a description...'), 255 | name: 'f', 256 | signatures: ['export declare const f: (a: number, b: number) => { [key: string]: number; }'], 257 | since: O.some('1.0.0'), 258 | examples: [ 259 | 'assert.deepStrictEqual(f(1, 2), { a: 1, b: 2 })', 260 | 'assert.deepStrictEqual(f(3, 4), { a: 3, b: 4 })' 261 | ], 262 | category: O.none 263 | } 264 | ]) 265 | ) 266 | }) 267 | 268 | it('should return a function declaration', () => { 269 | const env = getTestEnv( 270 | `/** 271 | * @since 1.0.0 272 | */ 273 | export function f(a: number, b: number): { [key: string]: number } { return { a, b } }` 274 | ) 275 | 276 | assert.deepStrictEqual( 277 | _.parseFunctions(env), 278 | E.right([ 279 | { 280 | _tag: 'Function', 281 | deprecated: false, 282 | description: O.none, 283 | name: 'f', 284 | signatures: ['export declare function f(a: number, b: number): { [key: string]: number }'], 285 | since: O.some('1.0.0'), 286 | examples: RA.empty, 287 | category: O.none 288 | } 289 | ]) 290 | ) 291 | }) 292 | 293 | it('should return a function with comments', () => { 294 | const env = getTestEnv( 295 | `/** 296 | * a description... 297 | * @since 1.0.0 298 | * @deprecated 299 | */ 300 | export function f(a: number, b: number): { [key: string]: number } { return { a, b } }` 301 | ) 302 | 303 | assert.deepStrictEqual( 304 | _.parseFunctions(env), 305 | E.right([ 306 | { 307 | _tag: 'Function', 308 | deprecated: true, 309 | description: O.some('a description...'), 310 | name: 'f', 311 | signatures: ['export declare function f(a: number, b: number): { [key: string]: number }'], 312 | since: O.some('1.0.0'), 313 | examples: RA.empty, 314 | category: O.none 315 | } 316 | ]) 317 | ) 318 | }) 319 | 320 | it('should handle overloadings', () => { 321 | const env = getTestEnv( 322 | `/** 323 | * a description... 324 | * @since 1.0.0 325 | * @deprecated 326 | */ 327 | export function f(a: Int, b: Int): { [key: string]: number } 328 | export function f(a: number, b: number): { [key: string]: number } 329 | export function f(a: any, b: any): { [key: string]: number } { return { a, b } }` 330 | ) 331 | 332 | assert.deepStrictEqual( 333 | _.parseFunctions(env), 334 | E.right([ 335 | { 336 | _tag: 'Function', 337 | name: 'f', 338 | description: O.some('a description...'), 339 | since: O.some('1.0.0'), 340 | deprecated: true, 341 | category: O.none, 342 | examples: RA.empty, 343 | signatures: [ 344 | 'export declare function f(a: Int, b: Int): { [key: string]: number }', 345 | 'export declare function f(a: number, b: number): { [key: string]: number }' 346 | ] 347 | } 348 | ]) 349 | ) 350 | }) 351 | }) 352 | 353 | describe.concurrent('parseTypeAlias', () => { 354 | it('should return a `TypeAlias`', () => { 355 | const env = getTestEnv( 356 | `/** 357 | * a description... 358 | * @since 1.0.0 359 | * @deprecated 360 | */ 361 | export type Option = None | Some` 362 | ) 363 | 364 | assertRight(pipe(env, _.parseTypeAliases), (actual) => 365 | assert.deepStrictEqual(actual, [ 366 | { 367 | _tag: 'TypeAlias', 368 | name: 'Option', 369 | description: O.some('a description...'), 370 | since: O.some('1.0.0'), 371 | deprecated: true, 372 | category: O.none, 373 | signature: 'export type Option = None | Some', 374 | examples: RA.empty 375 | } 376 | ]) 377 | ) 378 | }) 379 | }) 380 | 381 | describe.concurrent('parseConstants', () => { 382 | it('should handle a constant value', () => { 383 | const env = getTestEnv( 384 | `/** 385 | * a description... 386 | * @since 1.0.0 387 | * @deprecated 388 | */ 389 | export const s: string = ''` 390 | ) 391 | 392 | assertRight(pipe(env, _.parseConstants), (actual) => 393 | assert.deepStrictEqual(actual, [ 394 | { 395 | _tag: 'Constant', 396 | name: 's', 397 | description: O.some('a description...'), 398 | since: O.some('1.0.0'), 399 | deprecated: true, 400 | category: O.none, 401 | signature: 'export declare const s: string', 402 | examples: RA.empty 403 | } 404 | ]) 405 | ) 406 | }) 407 | 408 | it('should support constants with default type parameters', () => { 409 | const env = getTestEnv( 410 | `/** 411 | * @since 1.0.0 412 | */ 413 | export const left: (l: E) => string = T.left` 414 | ) 415 | 416 | assertRight(pipe(env, _.parseConstants), (actual) => 417 | assert.deepStrictEqual(actual, [ 418 | { 419 | _tag: 'Constant', 420 | name: 'left', 421 | description: O.none, 422 | since: O.some('1.0.0'), 423 | deprecated: false, 424 | category: O.none, 425 | signature: 'export declare const left: (l: E) => string', 426 | examples: RA.empty 427 | } 428 | ]) 429 | ) 430 | }) 431 | 432 | it('should support untyped constants', () => { 433 | const env = getTestEnv( 434 | ` 435 | class A {} 436 | /** 437 | * @since 1.0.0 438 | */ 439 | export const empty = new A()` 440 | ) 441 | 442 | assertRight(pipe(env, _.parseConstants), (actual) => 443 | assert.deepStrictEqual(actual, [ 444 | { 445 | _tag: 'Constant', 446 | name: 'empty', 447 | description: O.none, 448 | since: O.some('1.0.0'), 449 | deprecated: false, 450 | category: O.none, 451 | signature: 'export declare const empty: A', 452 | examples: RA.empty 453 | } 454 | ]) 455 | ) 456 | }) 457 | 458 | it('should handle constants with typeof annotations', () => { 459 | const env = getTestEnv( 460 | ` const task: { a: number } = { 461 | a: 1 462 | } 463 | /** 464 | * @since 1.0.0 465 | */ 466 | export const taskSeq: typeof task = { 467 | ...task, 468 | ap: (mab, ma) => () => mab().then(f => ma().then(a => f(a))) 469 | }` 470 | ) 471 | 472 | assertRight(pipe(env, _.parseConstants), (actual) => 473 | assert.deepStrictEqual(actual, [ 474 | { 475 | _tag: 'Constant', 476 | deprecated: false, 477 | description: O.none, 478 | name: 'taskSeq', 479 | signature: 'export declare const taskSeq: { a: number; }', 480 | since: O.some('1.0.0'), 481 | examples: RA.empty, 482 | category: O.none 483 | } 484 | ]) 485 | ) 486 | }) 487 | 488 | it('should not include variables declared in for loops', () => { 489 | const env = getTestEnv( 490 | ` const object = { a: 1, b: 2, c: 3 }; 491 | 492 | for (const property in object) { 493 | console.log(property); 494 | }` 495 | ) 496 | 497 | assertRight(pipe(env, _.parseConstants), (actual) => assert.deepStrictEqual(actual, [])) 498 | }) 499 | }) 500 | 501 | describe.concurrent('parseClasses', () => { 502 | it('should raise an error if the class is anonymous', () => { 503 | const env = getTestEnv(`export class {}`) 504 | 505 | assertLeft(pipe(env, _.parseClasses), (error) => assert.strictEqual(error, 'Missing class name in module test')) 506 | }) 507 | 508 | it('should raise an error if an `@since` tag is missing in a module', () => { 509 | const env = getTestEnv(`export class MyClass {}`) 510 | 511 | assertLeft(pipe(env, _.parseClasses), (error) => 512 | assert.strictEqual(error, 'Missing @since tag in test#MyClass documentation') 513 | ) 514 | }) 515 | 516 | it('should raise an error if `@since` is missing in a property', () => { 517 | const env = getTestEnv( 518 | `/** 519 | * @since 1.0.0 520 | */ 521 | export class MyClass { 522 | readonly _A!: A 523 | }` 524 | ) 525 | 526 | assertLeft(pipe(env, _.parseClasses), (error) => 527 | assert.strictEqual(error, 'Missing @since tag in test#MyClass#_A documentation') 528 | ) 529 | }) 530 | 531 | it('should skip ignored properties', () => { 532 | const env = getTestEnv( 533 | `/** 534 | * @since 1.0.0 535 | */ 536 | export class MyClass { 537 | /** 538 | * @ignore 539 | */ 540 | readonly _A!: A 541 | }` 542 | ) 543 | 544 | assertRight(pipe(env, _.parseClasses), (actual) => 545 | assert.deepStrictEqual(actual, [ 546 | { 547 | _tag: 'Class', 548 | name: 'MyClass', 549 | description: O.none, 550 | since: O.some('1.0.0'), 551 | deprecated: false, 552 | category: O.none, 553 | examples: RA.empty, 554 | signature: 'export declare class MyClass', 555 | methods: RA.empty, 556 | staticMethods: RA.empty, 557 | properties: RA.empty 558 | } 559 | ]) 560 | ) 561 | }) 562 | 563 | it('should skip the constructor body', () => { 564 | const env = getTestEnv( 565 | `/** 566 | * description 567 | * @since 1.0.0 568 | */ 569 | export class C { constructor() {} }` 570 | ) 571 | 572 | assertRight(pipe(env, _.parseClasses), (actual) => 573 | assert.deepStrictEqual(actual, [ 574 | { 575 | _tag: 'Class', 576 | name: 'C', 577 | description: O.some('description'), 578 | since: O.some('1.0.0'), 579 | deprecated: false, 580 | category: O.none, 581 | examples: RA.empty, 582 | signature: 'export declare class C { constructor() }', 583 | methods: RA.empty, 584 | staticMethods: RA.empty, 585 | properties: RA.empty 586 | } 587 | ]) 588 | ) 589 | }) 590 | 591 | it('should get a constructor declaration signature', () => { 592 | const env = getTestEnv(` 593 | /** 594 | * @since 1.0.0 595 | */ 596 | declare class A { 597 | constructor() 598 | } 599 | `) 600 | 601 | const constructorDeclaration = env.sourceFile.getClass('A')!.getConstructors()[0] 602 | 603 | assert.deepStrictEqual(_.getConstructorDeclarationSignature(constructorDeclaration), 'constructor()') 604 | }) 605 | 606 | it('should handle non-readonly properties', () => { 607 | const env = getTestEnv( 608 | `/** 609 | * description 610 | * @since 1.0.0 611 | */ 612 | export class C { 613 | /** 614 | * @since 1.0.0 615 | */ 616 | a: string 617 | }` 618 | ) 619 | 620 | assertRight(pipe(env, _.parseClasses), (actual) => 621 | assert.deepStrictEqual(actual, [ 622 | { 623 | _tag: 'Class', 624 | name: 'C', 625 | description: O.some('description'), 626 | since: O.some('1.0.0'), 627 | deprecated: false, 628 | category: O.none, 629 | examples: RA.empty, 630 | signature: 'export declare class C', 631 | methods: RA.empty, 632 | staticMethods: RA.empty, 633 | properties: [ 634 | { 635 | name: 'a', 636 | description: O.none, 637 | since: O.some('1.0.0'), 638 | deprecated: false, 639 | category: O.none, 640 | examples: RA.empty, 641 | signature: 'a: string' 642 | } 643 | ] 644 | } 645 | ]) 646 | ) 647 | }) 648 | 649 | it('should return a `Class`', () => { 650 | const env = getTestEnv( 651 | `/** 652 | * a class description... 653 | * @since 1.0.0 654 | * @deprecated 655 | */ 656 | export class Test { 657 | /** 658 | * a property... 659 | * @since 1.1.0 660 | * @deprecated 661 | */ 662 | readonly a: string 663 | private readonly b: number 664 | /** 665 | * a static method description... 666 | * @since 1.1.0 667 | * @deprecated 668 | */ 669 | static f(): void {} 670 | constructor(readonly value: string) { } 671 | /** 672 | * a method description... 673 | * @since 1.1.0 674 | * @deprecated 675 | */ 676 | g(a: number, b: number): { [key: string]: number } { 677 | return { a, b } 678 | } 679 | }` 680 | ) 681 | 682 | assertRight(pipe(env, _.parseClasses), (actual) => 683 | assert.deepStrictEqual(actual, [ 684 | { 685 | _tag: 'Class', 686 | name: 'Test', 687 | description: O.some('a class description...'), 688 | since: O.some('1.0.0'), 689 | deprecated: true, 690 | category: O.none, 691 | examples: RA.empty, 692 | signature: 'export declare class Test { constructor(readonly value: string) }', 693 | methods: [ 694 | { 695 | name: 'g', 696 | description: O.some('a method description...'), 697 | since: O.some('1.1.0'), 698 | deprecated: true, 699 | category: O.none, 700 | examples: RA.empty, 701 | signatures: ['g(a: number, b: number): { [key: string]: number }'] 702 | } 703 | ], 704 | staticMethods: [ 705 | { 706 | name: 'f', 707 | description: O.some('a static method description...'), 708 | since: O.some('1.1.0'), 709 | deprecated: true, 710 | category: O.none, 711 | examples: RA.empty, 712 | signatures: ['static f(): void'] 713 | } 714 | ], 715 | properties: [ 716 | { 717 | name: 'a', 718 | description: O.some('a property...'), 719 | since: O.some('1.1.0'), 720 | deprecated: true, 721 | category: O.none, 722 | signature: 'readonly a: string', 723 | examples: RA.empty 724 | } 725 | ] 726 | } 727 | ]) 728 | ) 729 | }) 730 | 731 | it('should handle method overloadings', () => { 732 | const env = getTestEnv( 733 | `/** 734 | * a class description... 735 | * @since 1.0.0 736 | * @deprecated 737 | */ 738 | export class Test { 739 | /** 740 | * a static method description... 741 | * @since 1.1.0 742 | * @deprecated 743 | */ 744 | static f(x: number): number 745 | static f(x: string): string 746 | static f(x: any): any {} 747 | constructor(readonly value: A) { } 748 | /** 749 | * a method description... 750 | * @since 1.1.0 751 | * @deprecated 752 | */ 753 | map(f: (a: number) => number): Test 754 | map(f: (a: string) => string): Test 755 | map(f: (a: any) => any): any { 756 | return new Test(f(this.value)) 757 | } 758 | }` 759 | ) 760 | 761 | assertRight(pipe(env, _.parseClasses), (actual) => 762 | assert.deepStrictEqual(actual, [ 763 | { 764 | _tag: 'Class', 765 | name: 'Test', 766 | description: O.some('a class description...'), 767 | since: O.some('1.0.0'), 768 | deprecated: true, 769 | category: O.none, 770 | examples: RA.empty, 771 | signature: 'export declare class Test { constructor(readonly value: A) }', 772 | methods: [ 773 | { 774 | name: 'map', 775 | description: O.some('a method description...'), 776 | since: O.some('1.1.0'), 777 | deprecated: true, 778 | category: O.none, 779 | examples: RA.empty, 780 | signatures: ['map(f: (a: number) => number): Test', 'map(f: (a: string) => string): Test'] 781 | } 782 | ], 783 | staticMethods: [ 784 | { 785 | name: 'f', 786 | description: O.some('a static method description...'), 787 | since: O.some('1.1.0'), 788 | deprecated: true, 789 | category: O.none, 790 | examples: RA.empty, 791 | signatures: ['static f(x: number): number', 'static f(x: string): string'] 792 | } 793 | ], 794 | properties: RA.empty 795 | } 796 | ]) 797 | ) 798 | }) 799 | 800 | it('should ignore internal/ignored methods (#42)', () => { 801 | const env = getTestEnv( 802 | `/** 803 | * a class description... 804 | * @since 1.0.0 805 | */ 806 | export class Test { 807 | /** 808 | * @since 0.0.1 809 | * @internal 810 | **/ 811 | private foo(): void {} 812 | /** 813 | * @since 0.0.1 814 | * @ignore 815 | **/ 816 | private bar(): void {} 817 | }` 818 | ) 819 | 820 | assertRight(pipe(env, _.parseClasses), (actual) => 821 | assert.deepStrictEqual(actual, [ 822 | { 823 | _tag: 'Class', 824 | name: 'Test', 825 | description: O.some('a class description...'), 826 | since: O.some('1.0.0'), 827 | deprecated: false, 828 | category: O.none, 829 | examples: RA.empty, 830 | signature: 'export declare class Test', 831 | methods: RA.empty, 832 | staticMethods: RA.empty, 833 | properties: RA.empty 834 | } 835 | ]) 836 | ) 837 | }) 838 | }) 839 | 840 | describe.concurrent('parseModuleDocumentation', () => { 841 | it('should return a description field and a deprecated field', () => { 842 | const env = getTestEnv( 843 | `/** 844 | * Manages the configuration settings for the widget 845 | * @deprecated 846 | * @since 1.0.0 847 | */ 848 | /** 849 | * @since 1.2.0 850 | */ 851 | export const a: number = 1` 852 | ) 853 | 854 | assertRight(pipe(env, _.parseModuleDocumentation), (actual) => 855 | assert.deepStrictEqual(actual, { 856 | name: 'test', 857 | description: O.some('Manages the configuration settings for the widget'), 858 | since: O.some('1.0.0'), 859 | deprecated: true, 860 | category: O.none, 861 | examples: RA.empty 862 | }) 863 | ) 864 | }) 865 | 866 | it('should return an error when documentation is enforced but no documentation is provided', () => { 867 | const env = getTestEnv('export const a: number = 1') 868 | 869 | assertLeft(pipe(env, _.parseModuleDocumentation), (actual) => 870 | assert.strictEqual(actual, 'Missing documentation in test module') 871 | ) 872 | }) 873 | 874 | it('should support absence of module documentation when no documentation is enforced', () => { 875 | const defaultEnv = getTestEnv('export const a: number = 1') 876 | const env: _.ParserEnv = { ...defaultEnv, config: { ...defaultEnv.config, enforceVersion: false } } 877 | 878 | assert.deepStrictEqual( 879 | pipe(env, _.parseModuleDocumentation), 880 | E.right({ 881 | name: 'test', 882 | description: O.none, 883 | since: O.none, 884 | deprecated: false, 885 | category: O.none, 886 | examples: RA.empty 887 | }) 888 | ) 889 | }) 890 | }) 891 | 892 | describe.concurrent('parseExports', () => { 893 | it('should return no `Export`s if the file is empty', () => { 894 | const env = getTestEnv('') 895 | 896 | assertRight(pipe(env, _.parseExports), (actual) => assert.deepStrictEqual(actual, RA.empty)) 897 | }) 898 | 899 | it('should handle renamimg', () => { 900 | const env = getTestEnv( 901 | `const a = 1; 902 | export { 903 | /** 904 | * @since 1.0.0 905 | */ 906 | a as b 907 | }` 908 | ) 909 | 910 | assertRight(pipe(env, _.parseExports), (actual) => 911 | assert.deepStrictEqual(actual, [ 912 | { 913 | _tag: 'Export', 914 | name: 'b', 915 | description: O.none, 916 | deprecated: false, 917 | since: O.some('1.0.0'), 918 | category: O.none, 919 | examples: RA.empty, 920 | signature: 'export declare const b: 1' 921 | } 922 | ]) 923 | ) 924 | }) 925 | 926 | it('should return an `Export`', () => { 927 | const env = getTestEnv( 928 | `export { 929 | /** 930 | * description_of_a 931 | * @since 1.0.0 932 | */ 933 | a, 934 | /** 935 | * description_of_b 936 | * @since 2.0.0 937 | */ 938 | b 939 | }` 940 | ) 941 | 942 | assertRight(pipe(env, _.parseExports), (actual) => 943 | assert.deepStrictEqual(actual, [ 944 | { 945 | _tag: 'Export', 946 | name: 'a', 947 | description: O.some('description_of_a'), 948 | since: O.some('1.0.0'), 949 | deprecated: false, 950 | category: O.none, 951 | signature: 'export declare const a: any', 952 | examples: RA.empty 953 | }, 954 | { 955 | _tag: 'Export', 956 | name: 'b', 957 | description: O.some('description_of_b'), 958 | since: O.some('2.0.0'), 959 | deprecated: false, 960 | category: O.none, 961 | signature: 'export declare const b: any', 962 | examples: RA.empty 963 | } 964 | ]) 965 | ) 966 | }) 967 | 968 | it('should raise an error if `@since` tag is missing in export', () => { 969 | const env = getTestEnv('export { a }') 970 | 971 | assertLeft(pipe(env, _.parseExports), (error) => assert.strictEqual(error, 'Missing a documentation in test')) 972 | }) 973 | 974 | it('should retrieve an export signature', () => { 975 | project.createSourceFile('a.ts', `export const a = 1`) 976 | const sourceFile = project.createSourceFile( 977 | 'b.ts', 978 | `import { a } from './a' 979 | const b = a 980 | export { 981 | /** 982 | * @since 1.0.0 983 | */ 984 | b 985 | }` 986 | ) 987 | 988 | assertRight( 989 | pipe( 990 | { 991 | path: ['test'], 992 | sourceFile, 993 | spawn: spawn, 994 | fileSystem: FS.FileSystem, 995 | logger: L.Logger, 996 | config, 997 | addFile: addFileToProject 998 | }, 999 | _.parseExports 1000 | ), 1001 | (actual) => 1002 | assert.deepStrictEqual(actual, [ 1003 | { 1004 | _tag: 'Export', 1005 | name: 'b', 1006 | description: O.none, 1007 | since: O.some('1.0.0'), 1008 | deprecated: false, 1009 | signature: 'export declare const b: 1', 1010 | category: O.none, 1011 | examples: RA.empty 1012 | } 1013 | ]) 1014 | ) 1015 | }) 1016 | }) 1017 | 1018 | describe.concurrent('parseModule', () => { 1019 | it('should raise an error if `@since` tag is missing', async () => { 1020 | const env = getTestEnv(`import * as assert from 'assert'`) 1021 | 1022 | assertLeft(pipe(env, _.parseModule), (error) => 1023 | assert.strictEqual(error, 'Missing documentation in test module') 1024 | ) 1025 | }) 1026 | 1027 | it('should not require an example for modules when `enforceExamples` is set to true (#38)', () => { 1028 | const env = getTestEnv(`/** 1029 | * This is the assert module. 1030 | * 1031 | * @since 1.0.0 1032 | */ 1033 | import * as assert from 'assert' 1034 | 1035 | /** 1036 | * This is the foo export. 1037 | * 1038 | * @example 1039 | * import { foo } from 'test' 1040 | * 1041 | * console.log(foo) 1042 | * 1043 | * @category foo 1044 | * @since 1.0.0 1045 | */ 1046 | export const foo = 'foo'`) 1047 | 1048 | assertRight(pipe({ ...env, config: { ...env.config, enforceExamples: true } }, _.parseModule), (actual) => 1049 | assert.deepStrictEqual(actual, { 1050 | name: 'test', 1051 | description: O.some('This is the assert module.'), 1052 | since: O.some('1.0.0'), 1053 | deprecated: false, 1054 | examples: RA.empty, 1055 | category: O.none, 1056 | path: ['test'], 1057 | classes: RA.empty, 1058 | interfaces: RA.empty, 1059 | functions: RA.empty, 1060 | typeAliases: RA.empty, 1061 | constants: [ 1062 | { 1063 | _tag: 'Constant', 1064 | name: 'foo', 1065 | description: O.some('This is the foo export.'), 1066 | since: O.some('1.0.0'), 1067 | deprecated: false, 1068 | examples: [`import { foo } from 'test'\n\nconsole.log(foo)`], 1069 | category: O.some('foo'), 1070 | signature: 'export declare const foo: "foo"' 1071 | } 1072 | ], 1073 | exports: RA.empty 1074 | }) 1075 | ) 1076 | }) 1077 | }) 1078 | 1079 | describe.concurrent('parseFile', () => { 1080 | it('should not parse a non-existent file', async () => { 1081 | const file = FS.File('non-existent.ts', '') 1082 | const project = new ast.Project({ useInMemoryFileSystem: true }) 1083 | 1084 | assertLeft( 1085 | await pipe( 1086 | { 1087 | spawn, 1088 | fileSystem: FS.FileSystem, 1089 | logger: L.Logger, 1090 | config, 1091 | addFile: addFileToProject 1092 | }, 1093 | _.parseFile(project)(file) 1094 | )(), 1095 | (error) => assert.strictEqual(error, 'Unable to locate file: non-existent.ts') 1096 | ) 1097 | }) 1098 | }) 1099 | 1100 | describe.concurrent('parseFiles', () => { 1101 | it('should parse an array of files', async () => { 1102 | const files = [ 1103 | FS.File( 1104 | 'test/fixtures/test1.ts', 1105 | ` 1106 | /** 1107 | * a description... 1108 | * 1109 | * @since 1.0.0 1110 | */ 1111 | export function f(a: number, b: number): { [key: string]: number } { 1112 | return { a, b } 1113 | } 1114 | ` 1115 | ), 1116 | FS.File( 1117 | 'test/fixtures/test2.ts', 1118 | ` 1119 | /** 1120 | * a description... 1121 | * 1122 | * @deprecated 1123 | * @since 1.0.0 1124 | */ 1125 | export function f(a: number, b: number): { [key: string]: number } { 1126 | return { a, b } 1127 | } 1128 | ` 1129 | ) 1130 | ] 1131 | 1132 | assertRight( 1133 | await pipe( 1134 | { 1135 | spawn, 1136 | fileSystem: FS.FileSystem, 1137 | logger: L.Logger, 1138 | config, 1139 | addFile: addFileToProject 1140 | }, 1141 | _.parseFiles(files) 1142 | )(), 1143 | (actual) => 1144 | assert.deepStrictEqual(actual, [ 1145 | { 1146 | name: 'test1', 1147 | path: ['test', 'fixtures', 'test1.ts'], 1148 | description: O.some('a description...'), 1149 | since: O.some('1.0.0'), 1150 | deprecated: false, 1151 | category: O.none, 1152 | examples: RA.empty, 1153 | classes: RA.empty, 1154 | constants: RA.empty, 1155 | exports: RA.empty, 1156 | interfaces: RA.empty, 1157 | typeAliases: RA.empty, 1158 | functions: [ 1159 | { 1160 | _tag: 'Function', 1161 | name: 'f', 1162 | description: O.some('a description...'), 1163 | since: O.some('1.0.0'), 1164 | deprecated: false, 1165 | category: O.none, 1166 | examples: RA.empty, 1167 | signatures: ['export declare function f(a: number, b: number): { [key: string]: number }'] 1168 | } 1169 | ] 1170 | } 1171 | ]) 1172 | ) 1173 | }) 1174 | }) 1175 | }) 1176 | 1177 | describe.concurrent('utils', () => { 1178 | describe.concurrent('getCommentInfo', () => { 1179 | it('should parse comment information', () => { 1180 | const env = getTestEnv('') 1181 | 1182 | const text = `/** 1183 | * description 1184 | * @category instances 1185 | * @since 1.0.0 1186 | */` 1187 | 1188 | assertRight(pipe(env, _.getCommentInfo('name')(text)), (actual) => 1189 | assert.deepStrictEqual(actual, { 1190 | description: O.some('description'), 1191 | since: O.some('1.0.0'), 1192 | category: O.some('instances'), 1193 | deprecated: false, 1194 | examples: RA.empty 1195 | }) 1196 | ) 1197 | }) 1198 | 1199 | it('should fail if an empty comment tag is provided', () => { 1200 | const env = getTestEnv('') 1201 | 1202 | const text = `/** 1203 | * @category 1204 | * @since 1.0.0 1205 | */` 1206 | 1207 | assertLeft(pipe(env, _.getCommentInfo('name')(text)), (error) => 1208 | assert.strictEqual(error, 'Missing @category value in test#name documentation') 1209 | ) 1210 | }) 1211 | 1212 | it('should require a description if `enforceDescriptions` is set to true', () => { 1213 | const env = getTestEnv('') 1214 | 1215 | const text = `/** 1216 | * @category instances 1217 | * @since 1.0.0 1218 | */` 1219 | 1220 | assertLeft( 1221 | pipe({ ...env, config: { ...env.config, enforceDescriptions: true } }, _.getCommentInfo('name')(text)), 1222 | (error) => assert.strictEqual(error, 'Missing description in test#name documentation') 1223 | ) 1224 | }) 1225 | 1226 | it('should require at least one example if `enforceExamples` is set to true', () => { 1227 | const env = getTestEnv('') 1228 | 1229 | const text = `/** 1230 | * description 1231 | * @category instances 1232 | * @since 1.0.0 1233 | */` 1234 | 1235 | assertLeft( 1236 | pipe({ ...env, config: { ...env.config, enforceExamples: true } }, _.getCommentInfo('name')(text)), 1237 | (error) => assert.strictEqual(error, 'Missing examples in test#name documentation') 1238 | ) 1239 | }) 1240 | 1241 | it('should require at least one non-empty example if `enforceExamples` is set to true', () => { 1242 | const env = getTestEnv('') 1243 | 1244 | const text = `/** 1245 | * description 1246 | * @example 1247 | * @category instances 1248 | * @since 1.0.0 1249 | */` 1250 | 1251 | assertLeft( 1252 | pipe({ ...env, config: { ...env.config, enforceExamples: true } }, _.getCommentInfo('name')(text)), 1253 | (error) => assert.strictEqual(error, 'Missing examples in test#name documentation') 1254 | ) 1255 | }) 1256 | 1257 | it('should allow no since tag if `enforceVersion` is set to false', () => { 1258 | const env = getTestEnv('') 1259 | 1260 | const text = `/** 1261 | * description 1262 | * @category instances 1263 | */` 1264 | 1265 | assertRight( 1266 | pipe({ ...env, config: { ...env.config, enforceVersion: false } }, _.getCommentInfo('name')(text)), 1267 | (actual) => 1268 | assert.deepStrictEqual(actual, { 1269 | description: O.some('description'), 1270 | since: O.none, 1271 | category: O.some('instances'), 1272 | deprecated: false, 1273 | examples: RA.empty 1274 | }) 1275 | ) 1276 | }) 1277 | }) 1278 | 1279 | it('parseComment', () => { 1280 | assert.deepStrictEqual(_.parseComment(''), { 1281 | description: O.none, 1282 | tags: {} 1283 | }) 1284 | 1285 | assert.deepStrictEqual(_.parseComment('/** description */'), { 1286 | description: O.some('description'), 1287 | tags: {} 1288 | }) 1289 | 1290 | assert.deepStrictEqual(_.parseComment('/** description\n * @since 1.0.0\n */'), { 1291 | description: O.some('description'), 1292 | tags: { 1293 | since: [O.some('1.0.0')] 1294 | } 1295 | }) 1296 | 1297 | assert.deepStrictEqual(_.parseComment('/** description\n * @deprecated\n */'), { 1298 | description: O.some('description'), 1299 | tags: { 1300 | deprecated: [O.none] 1301 | } 1302 | }) 1303 | 1304 | assert.deepStrictEqual(_.parseComment('/** description\n * @category instance\n */'), { 1305 | description: O.some('description'), 1306 | tags: { 1307 | category: [O.some('instance')] 1308 | } 1309 | }) 1310 | }) 1311 | 1312 | it('stripImportTypes', () => { 1313 | assert.strictEqual( 1314 | _.stripImportTypes( 1315 | '{ (refinement: import("/Users/giulio/Documents/Projects/github/fp-ts/src/function").Refinement, onFalse: (a: A) => E): (ma: Either) => Either; (predicate: Predicate, onFalse: (a: A) => E): (ma: Either) => Either; }' 1316 | ), 1317 | '{ (refinement: Refinement, onFalse: (a: A) => E): (ma: Either) => Either; (predicate: Predicate, onFalse: (a: A) => E): (ma: Either) => Either; }' 1318 | ) 1319 | assert.strictEqual( 1320 | _.stripImportTypes( 1321 | '{ (refinementWithIndex: import("/Users/giulio/Documents/Projects/github/fp-ts/src/FilterableWithIndex").RefinementWithIndex): (fa: A[]) => B[]; (predicateWithIndex: import("/Users/giulio/Documents/Projects/github/fp-ts/src/FilterableWithIndex").PredicateWithIndex): (fa: A[]) => A[]; }' 1322 | ), 1323 | '{ (refinementWithIndex: RefinementWithIndex): (fa: A[]) => B[]; (predicateWithIndex: PredicateWithIndex): (fa: A[]) => A[]; }' 1324 | ) 1325 | }) 1326 | }) 1327 | }) 1328 | --------------------------------------------------------------------------------