├── .gitattributes ├── .gitignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.MD ├── __fixtures__ ├── basic-with-other │ ├── src │ │ ├── excluded │ │ │ ├── bar.ts │ │ │ ├── foo.ts │ │ │ └── other-excluded │ │ │ │ └── data.json │ │ ├── main.js │ │ ├── main.ts │ │ ├── other │ │ │ ├── data.json │ │ │ ├── logic.js │ │ │ └── view.html │ │ └── second.ts │ └── tsconfig.json ├── basic │ ├── src │ │ ├── excluded │ │ │ ├── bar.ts │ │ │ └── foo.ts │ │ ├── main.ts │ │ └── second.ts │ └── tsconfig.json └── bundle │ ├── ambient-declaration │ ├── global.ts │ ├── internal.ts │ └── main.ts │ ├── circular-reference-bug │ └── main.ts │ ├── complex │ ├── data.json │ ├── main.ts │ └── module │ │ ├── classes.ts │ │ ├── enums.ts │ │ ├── export-equals.ts │ │ ├── external.ts │ │ ├── functions-variables.ts │ │ ├── interfaces.ts │ │ └── namespaces.ts │ ├── duplicate │ └── main.ts │ ├── external-name-conflict │ ├── interface.ts │ └── main.ts │ ├── global-name-conflict │ ├── get-real-date.ts │ ├── main.ts │ ├── my-date.ts │ └── my-promise.ts │ ├── namespace-merge │ ├── interfaces.ts │ └── main.ts │ └── tsconfig.json ├── __tests__ ├── builder.ts ├── bundle-addon.ts ├── clean-addon.ts └── copy-addon.ts ├── build.ts ├── jest.config.js ├── package.json ├── src ├── builder.ts ├── bundle-addon │ ├── declaration-collector.ts │ ├── declaration-registrar.ts │ ├── directive-collector.ts │ ├── external-augmentation-collector.ts │ ├── index.ts │ ├── printer.ts │ ├── symbol-collector.ts │ ├── syntax-check.ts │ └── syntax-retrieval.ts ├── clean-addon.ts ├── copy-addon.ts ├── index.ts ├── interfaces.ts ├── ts-internals.d.ts └── utils │ ├── fs.ts │ ├── log.ts │ ├── manipulation.ts │ └── path.ts ├── tsconfig.json └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | # Tells Github UI to allow comments in json files 2 | *.json linguist-language=JSON-with-Comments 3 | 4 | # Tells Github to not take js config root files into acccount for language statistics 5 | /*.js linguist-detectable=false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build output 2 | dist 3 | 4 | # Environment variables file 5 | .env 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Dependencies directory 15 | node_modules/ 16 | 17 | # Output of 'npm pack' 18 | *.tgz 19 | 20 | # Yarn Integrity file 21 | .yarn-integrity 22 | 23 | # Optional npm cache directory 24 | .npm 25 | 26 | # OS files 27 | .DS_Store 28 | Thumbs.db 29 | [Dd]esktop.ini 30 | 31 | # Vscode 32 | .vscode/* 33 | !.vscode/settings.json 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json 37 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "useTabs": true, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "es5", 8 | "bracketSpacing": true, 9 | "arrowParens": "always", 10 | "proseWrap": "preserve", 11 | "endOfLine": "lf" 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "ms-vscode.vscode-typescript-tslint-plugin" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "npm.packageManager": "yarn", 4 | "prettier.requireConfig": true, 5 | "files.associations": { 6 | "tslint.json": "jsonc" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [2.3.0](https://github.com/jeremyben/tsc-prog/compare/v2.2.1...v2.3.0) (2023-07-29) 6 | 7 | 8 | ### Features 9 | 10 | * option in bundle to add extra declarations ([0872e6b](https://github.com/jeremyben/tsc-prog/commit/0872e6b3c2f65452bbc1fbb1580da2491a6092bf)) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * **copy:** more straightforward and reliable way to prevent copy overwriting ([1cd622f](https://github.com/jeremyben/tsc-prog/commit/1cd622f6612fa2b8faf42b95f5964ae54d91f0ea)) 16 | * stop showing warning for references to globalThis which has no declaration ([f67bed7](https://github.com/jeremyben/tsc-prog/commit/f67bed7fb863fced5a53adcc080e391bbc3117fd)) 17 | * typings break with new typescript version ([7f0f774](https://github.com/jeremyben/tsc-prog/commit/7f0f774dbbfb9c75b8d2f4cfc5a74610544624b3)) 18 | * update tsconfig interface ([fedd99e](https://github.com/jeremyben/tsc-prog/commit/fedd99e1e07df06dd394b44b36f5d9c6dba4afa0)) 19 | 20 | ### [2.2.1](https://github.com/jeremyben/tsc-prog/compare/v2.2.0...v2.2.1) (2020-04-19) 21 | 22 | 23 | ### Bug Fixes 24 | 25 | * **bundle:** infinite loop due to circular reference ([3ca2cb8](https://github.com/jeremyben/tsc-prog/commit/3ca2cb8b1562362a7d02cfd61a2dafd7a836748a)) 26 | 27 | ## [2.2.0](https://github.com/jeremyben/tsc-prog/compare/v2.1.1...v2.2.0) (2020-04-04) 28 | 29 | 30 | ### Features 31 | 32 | * bundle globals and external library augmentations with options to switch off ([644784e](https://github.com/jeremyben/tsc-prog/commit/644784e5d41f196492163f571a25c97c53108ee1)) 33 | 34 | ### [2.1.1](https://github.com/jeremyben/tsc-prog/compare/v2.1.0...v2.1.1) (2020-02-16) 35 | 36 | 37 | ### Bug Fixes 38 | 39 | * **bundle:** properly hide non exported declarations ([ec1a760](https://github.com/jeremyben/tsc-prog/commit/ec1a760af87687ee819fcf2029cd68934d92bdb5)) 40 | 41 | ## [2.1.0](https://github.com/jeremyben/tsc-prog/compare/v2.0.3...v2.1.0) (2019-11-27) 42 | 43 | 44 | ### Features 45 | 46 | * expose custom compiler host option ([209470b](https://github.com/jeremyben/tsc-prog/commit/209470b09221eb5bc44c98f3c6a2a3343a301ff2)) 47 | * option to bundle declaration files ([35b6fd9](https://github.com/jeremyben/tsc-prog/commit/35b6fd9285f8cf7dafef9cecf0256aecc8a8e33a)) 48 | 49 | 50 | ### Bug Fixes 51 | 52 | * **bundle:** global name conflicts are more accurately handled ([fd2935b](https://github.com/jeremyben/tsc-prog/commit/fd2935bfb1f355aaef924f59b22e97c6d0b6d0b1)) 53 | * accept absolute path in bundle entrypoint ([ad19633](https://github.com/jeremyben/tsc-prog/commit/ad19633e1d66745c44617ff2fa7573e9542c60f6)) 54 | * copy options takes previous exclude pattern into account ([a1c8f07](https://github.com/jeremyben/tsc-prog/commit/a1c8f07cec1aa8f0160ba061cd60ae9ed83d049d)) 55 | * overwrite protection in copy option did not work without listEmittedFiles ([a1f07b3](https://github.com/jeremyben/tsc-prog/commit/a1f07b32902efa82667f55232611161fd0c6ff30)) 56 | * throw on errors and failures instead of just logging ([ac1c876](https://github.com/jeremyben/tsc-prog/commit/ac1c87640f2f24447b1083b6c771e4c780e5c34a)) 57 | * use colors in logs only if the output is TTY ([1b43895](https://github.com/jeremyben/tsc-prog/commit/1b438954f879c503d34b33ce79fe670e308c5df1)) 58 | 59 | ### [2.0.3](https://github.com/jeremyben/tsc-prog/compare/v2.0.2...v2.0.3) (2019-10-13) 60 | 61 | 62 | ### Bug Fixes 63 | 64 | * expose interfaces ([b3d550d](https://github.com/jeremyben/tsc-prog/commit/b3d550dfd9b93575aa9bc93ddcb8e0190995cad0)) 65 | * increase pause on windows platform after clean ([150813b](https://github.com/jeremyben/tsc-prog/commit/150813b1bdf79d0ccf33ce40e9994f3fc0d6af0c)) 66 | 67 | ### [2.0.2](https://github.com/jeremyben/tsc-prog/compare/v2.0.1...v2.0.2) (2019-08-15) 68 | 69 | 70 | ### Bug Fixes 71 | 72 | * do not copy declarationDir into outDir ([06de1a1](https://github.com/jeremyben/tsc-prog/commit/06de1a1)) 73 | 74 | 75 | 76 | ### [2.0.1](https://github.com/jeremyben/tsc-prog/compare/v2.0.0...v2.0.1) (2019-08-06) 77 | 78 | 79 | ### Bug Fixes 80 | 81 | * outDir was recursively copied into itself ([4b9550c](https://github.com/jeremyben/tsc-prog/commit/4b9550c)) 82 | 83 | 84 | 85 | ## [2.0.0](https://github.com/jeremyben/tsc-prog/compare/v1.3.0...v2.0.0) (2019-07-26) 86 | 87 | 88 | ### Bug Fixes 89 | 90 | * use pretty compiler option for diagnostics ([3c8db99](https://github.com/jeremyben/tsc-prog/commit/3c8db99)) 91 | 92 | 93 | ### BREAKING CHANGES 94 | 95 | * betterDiagnostics option has been removed 96 | 97 | 98 | 99 | ## [1.3.0](https://github.com/jeremyben/tsc-prog/compare/v1.2.2...v1.3.0) (2019-07-26) 100 | 101 | 102 | ### Bug Fixes 103 | 104 | * pause on windows after cleaning to help with file handles ([584c0c3](https://github.com/jeremyben/tsc-prog/commit/584c0c3)) 105 | * show some colors in logs ([06d8bbd](https://github.com/jeremyben/tsc-prog/commit/06d8bbd)) 106 | 107 | 108 | ### Features 109 | 110 | * copy non typescript files to outdir ([332f0f0](https://github.com/jeremyben/tsc-prog/commit/332f0f0)) 111 | 112 | 113 | 114 | ### [1.2.2](https://github.com/jeremyben/tsc-prog/compare/v1.2.1...v1.2.2) (2019-07-22) 115 | 116 | 117 | ### Bug Fixes 118 | 119 | * compiler list files options (pre-compile and emitted) ([e882ef8](https://github.com/jeremyben/tsc-prog/commit/e882ef8)) 120 | * protect parents of rootDir in clean option ([c7131f5](https://github.com/jeremyben/tsc-prog/commit/c7131f5)) 121 | 122 | 123 | 124 | ### [1.2.1](https://github.com/jeremyben/tsc-prog/compare/v1.2.0...v1.2.1) (2019-07-21) 125 | 126 | 127 | ### Bug Fixes 128 | 129 | * use compiler options from tsconfig.json schema, not ts module ([c651fcc](https://github.com/jeremyben/tsc-prog/commit/c651fcc)) 130 | 131 | 132 | 133 | ## [1.2.0](https://github.com/jeremyben/tsc-prog/compare/v1.1.0...v1.2.0) (2019-07-20) 134 | 135 | 136 | ### Bug Fixes 137 | 138 | * correctly assign compiler options with the right interfaces from ts module ([5e09382](https://github.com/jeremyben/tsc-prog/commit/5e09382)) 139 | 140 | 141 | ### Features 142 | 143 | * clean option is protected against deleting sensitive folders ([cba911d](https://github.com/jeremyben/tsc-prog/commit/cba911d)) 144 | 145 | 146 | 147 | ## [1.1.0](https://github.com/jeremyben/tsc-prog/compare/v1.0.0...v1.1.0) (2019-07-17) 148 | 149 | 150 | ### Features 151 | 152 | * option to recursively clean files and folders before emitting ([9d406b8](https://github.com/jeremyben/tsc-prog/commit/9d406b8)) 153 | 154 | 155 | 156 | ## 1.0.0 (2019-07-17) 157 | 158 | 159 | ### Features 160 | 161 | * program creation, files emitting, diagnostics logging and formatting ([a27dc50](https://github.com/jeremyben/tsc-prog/commit/a27dc50)) 162 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jeremy Bensimon 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 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # `tsc-prog` 2 | 3 | Build your TypeScript projects programmatically. 4 | 5 | `tsc-prog` offers flexiblity and convenient options for your more complex production builds (less suited for development builds). 6 | 7 | ## Getting started 8 | 9 | ```bash 10 | npm i -D tsc-prog 11 | yarn add -D tsc-prog 12 | ``` 13 | 14 | _`tsc-prog` has no dependency. You just need typescript as a peer dependency._ 15 | 16 | ### You simply need to build 👷‍ 17 | 18 | Use **`tsc.build`**. Specify the `basePath` first, and either inherit from a tsconfig file or create a config from scratch. 19 | 20 | ```js 21 | const tsc = require('tsc-prog') 22 | 23 | tsc.build({ 24 | basePath: __dirname, // always required, used for relative paths 25 | configFilePath: 'tsconfig.json', // config to inherit from (optional) 26 | compilerOptions: { 27 | rootDir: 'src', 28 | outDir: 'dist', 29 | declaration: true, 30 | skipLibCheck: true, 31 | }, 32 | include: ['src/**/*'], 33 | exclude: ['**/*.test.ts', '**/*.spec.ts'], 34 | }) 35 | ``` 36 | 37 | _You can have a look at all the parameters **[here](./src/interfaces.ts)**._ 38 | 39 | ### You need more access 👨‍🏭 40 | 41 | The `tsc.build` function is made of the two following steps, which you can have access to : 42 | 43 | - [Program](https://github.com/microsoft/TypeScript/wiki/Architectural-Overview#data-structures) creation with **`tsc.createProgramFromConfig`**. 44 | - Emitting files from program with **`tsc.emit`**. 45 | 46 | ```js 47 | const tsc = require('tsc-prog') 48 | 49 | // Create the program 50 | const program = tsc.createProgramFromConfig({ 51 | basePath: process.cwd(), 52 | configFilePath: 'tsconfig.json', 53 | }) 54 | 55 | // Do what you want with the program 56 | 57 | // Actually compile typescript files 58 | tsc.emit(program, { copyOtherToOutDir: true }) 59 | ``` 60 | 61 | ## Addons 62 | 63 | ### Clean 🧹 64 | 65 | _Helps to address [this issue](https://github.com/microsoft/TypeScript/issues/16057)._ 66 | 67 | We frequently need to delete the emitted files from a previous build, so a **`clean`** option recursively removes folders and files : 68 | 69 | ```js 70 | tsc.build({ 71 | basePath: __dirname, 72 | configFilePath: 'tsconfig.json', 73 | clean: ['dist'], // accepts relative paths to `basePath` or absolute paths 74 | }) 75 | ``` 76 | 77 | You can also directly specify common targets from your compiler options : 78 | 79 | ```js 80 | tsc.build({ 81 | basePath: __dirname, 82 | configFilePath: 'tsconfig.json', 83 | clean: { outDir: true, declarationDir: true }, 84 | }) 85 | ``` 86 | 87 | ###### Protections 88 | 89 | The `clean` option protects you against deleting the following folders : 90 | 91 | - the specified `basePath` and all its parents (up to the root folder). 92 | - the current working directory and all its parents (up to the root folder). 93 | - the `rootDir` path if specified in the compiler options and all its parents (up to the root folder). 94 | 95 | ### Copy non-typescript files 🗂️ 96 | 97 | _Helps to address [this issue](https://github.com/Microsoft/TypeScript/issues/30835)._ 98 | 99 | The **`copyOtherToOutDir`** option allows you to copy other files to `outDir` (well it says so) : 100 | 101 | ```js 102 | tsc.build({ 103 | basePath: __dirname, 104 | configFilePath: 'tsconfig.json', 105 | compilerOptions: { 106 | outDir: 'dist', // must be set 107 | }, 108 | copyOtherToOutDir: true, 109 | exclude: ['**/somedir'], // taken into account 110 | }) 111 | ``` 112 | 113 | This option is protected against overwriting files emitted by the compiler, like same name `.js` files (could happen). 114 | 115 | ### Bundle type definitions 🛍️ 116 | 117 | _Helps to address [this issue](https://github.com/microsoft/TypeScript/issues/4433)._ 118 | 119 | Rollup your emitted `.d.ts` files into a single one with **`bundleDeclaration`** option. 120 | 121 | ```js 122 | tsc.build({ 123 | basePath: __dirname, 124 | configFilePath: 'tsconfig.json', 125 | compilerOptions: { 126 | rootDir: 'src', 127 | outDir: 'dist', 128 | declaration: true // must be set 129 | }, 130 | bundleDeclaration: { 131 | entryPoint: 'index.d.ts', // relative to the OUTPUT directory ('dist' here) 132 | }, 133 | }) 134 | ``` 135 | 136 | #### Bundling options 137 | 138 | ```js 139 | tsc.build({ 140 | // ... 141 | bundleDeclaration: { 142 | entryPoint: 'index.d.ts', 143 | fallbackOnError: false, // default: true 144 | globals: false // default: true 145 | augmentations: false // default: true 146 | } 147 | }) 148 | ``` 149 | 150 | - `fallbackOnError` option is a safety mecanism that generates the original unbundled definition files if any error happens during the bundling process. 151 | 152 | - `globals` option can be switched to `false` to discard global declarations. 153 | 154 | - `augmentations` option can be switched to `false` to discard external library augmentations. 155 | 156 | #### Notes on bundling 🗣️ 157 | 158 | I recommend you still check the final `.d.ts` output, declaration bundling being very complex, with a lot of edge cases and issues such as name conflict and handling of external libraries. 159 | 160 | `tsc-prog` does its best to acknowledge every edge case. It covers ones that similar tools don't and probably vice versa. Don't hesitate to review [API Extractor](https://api-extractor.com/) to see if it works better with your program. 161 | -------------------------------------------------------------------------------- /__fixtures__/basic-with-other/src/excluded/bar.ts: -------------------------------------------------------------------------------- 1 | import { pseudoRandomBytes } from 'crypto' 2 | 3 | const bar = pseudoRandomBytes(64).toString('utf8') 4 | -------------------------------------------------------------------------------- /__fixtures__/basic-with-other/src/excluded/foo.ts: -------------------------------------------------------------------------------- 1 | import { pseudoRandomBytes } from 'crypto' 2 | 3 | const foo = pseudoRandomBytes(32).toString('utf8') 4 | -------------------------------------------------------------------------------- /__fixtures__/basic-with-other/src/excluded/other-excluded/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonFixture": true, 3 | "excluded": true 4 | } 5 | -------------------------------------------------------------------------------- /__fixtures__/basic-with-other/src/main.js: -------------------------------------------------------------------------------- 1 | // Dummy file to test overwrite protection with `copyOtherToOutDir` 2 | -------------------------------------------------------------------------------- /__fixtures__/basic-with-other/src/main.ts: -------------------------------------------------------------------------------- 1 | import { hello } from './second' 2 | 3 | export const variable = 1 4 | 5 | export const greet = () => { 6 | console.log(hello) 7 | } 8 | -------------------------------------------------------------------------------- /__fixtures__/basic-with-other/src/other/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonFixture": null, 3 | "default": 1 4 | } 5 | -------------------------------------------------------------------------------- /__fixtures__/basic-with-other/src/other/logic.js: -------------------------------------------------------------------------------- 1 | module.exports = { jsFixture: true } 2 | -------------------------------------------------------------------------------- /__fixtures__/basic-with-other/src/other/view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HTML Fixture 6 | 7 | 8 |

Yolo

9 | 10 | 11 | -------------------------------------------------------------------------------- /__fixtures__/basic-with-other/src/second.ts: -------------------------------------------------------------------------------- 1 | export const hello = 'Hello World' 2 | -------------------------------------------------------------------------------- /__fixtures__/basic-with-other/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "lib": ["es2017"], 6 | "rootDir": "./src", 7 | "outDir": "./dist", 8 | "strict": true, 9 | "moduleResolution": "node", 10 | "esModuleInterop": true, 11 | "resolveJsonModule": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /__fixtures__/basic/src/excluded/bar.ts: -------------------------------------------------------------------------------- 1 | import { pseudoRandomBytes } from 'crypto' 2 | 3 | const bar = pseudoRandomBytes(64).toString('utf8') 4 | -------------------------------------------------------------------------------- /__fixtures__/basic/src/excluded/foo.ts: -------------------------------------------------------------------------------- 1 | import { pseudoRandomBytes } from 'crypto' 2 | 3 | const foo = pseudoRandomBytes(32).toString('utf8') 4 | -------------------------------------------------------------------------------- /__fixtures__/basic/src/main.ts: -------------------------------------------------------------------------------- 1 | import { hello } from './second' 2 | 3 | export const variable = 1 4 | 5 | export const greet = () => { 6 | console.log(hello) 7 | } 8 | -------------------------------------------------------------------------------- /__fixtures__/basic/src/second.ts: -------------------------------------------------------------------------------- 1 | export const hello = 'Hello World' 2 | -------------------------------------------------------------------------------- /__fixtures__/basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "lib": ["es2017"], 6 | "rootDir": "./src", 7 | "outDir": "./dist", 8 | "strict": true, 9 | "moduleResolution": "node", 10 | "esModuleInterop": true, 11 | "resolveJsonModule": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /__fixtures__/bundle/ambient-declaration/global.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Global namespace. 3 | */ 4 | declare namespace GlobalNamespace { 5 | interface Message { 6 | text: string 7 | } 8 | } 9 | 10 | declare module 'os' { 11 | interface Augmentation { 12 | version: string 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /__fixtures__/bundle/ambient-declaration/internal.ts: -------------------------------------------------------------------------------- 1 | export interface InternalInterface { 2 | text: string 3 | } 4 | -------------------------------------------------------------------------------- /__fixtures__/bundle/ambient-declaration/main.ts: -------------------------------------------------------------------------------- 1 | import { ExternalInterface, ExternalNamespace } from 'http' 2 | import { InternalInterface } from './internal' 3 | import { Augmentation } from 'os' 4 | 5 | export { CpuInfo } from 'os' 6 | 7 | export const message: GlobalNamespace.Message = { text: 'hello', type: 'new' } 8 | 9 | export const glob: GlobalInterface = { example: '' } 10 | 11 | export const ext: ExternalInterface = { hello: '' } 12 | 13 | export const ext2: ExternalNamespace.Interface = { hi: '' } 14 | 15 | export const int: InternalInterface = { text: 'hello', number: 1 } 16 | 17 | export const aug: Augmentation = { version: '1' } 18 | 19 | export { ExternalNamespace, GlobalInterface } 20 | 21 | declare global { 22 | /** 23 | * Global namespace augmentation. 24 | */ 25 | namespace GlobalNamespace { 26 | interface Message { 27 | type: string 28 | } 29 | } 30 | 31 | interface GlobalInterface { 32 | example: string 33 | } 34 | } 35 | 36 | declare module 'http' { 37 | export interface ExternalInterface { 38 | hello: string 39 | } 40 | 41 | export namespace ExternalNamespace { 42 | interface Interface { 43 | hi: string 44 | } 45 | } 46 | } 47 | 48 | declare module './internal' { 49 | export interface InternalInterface { 50 | number: number 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /__fixtures__/bundle/circular-reference-bug/main.ts: -------------------------------------------------------------------------------- 1 | export class User { 2 | name: string 3 | 4 | constructor(data: T) { 5 | this.name = data.name 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /__fixtures__/bundle/complex/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonFixture": null, 3 | "default": 1 4 | } 5 | -------------------------------------------------------------------------------- /__fixtures__/bundle/complex/main.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable: no-implicit-dependencies 2 | 3 | export * from './module/external' 4 | 5 | export { ClassType, GenericDecorator, PromiseLike } from './module/interfaces' 6 | export type KV_1 = { [key: string]: any } 7 | 8 | // export { PromiseLike as default } from './module/interfaces' 9 | 10 | export * from './module/functions-variables' 11 | export { isAsync as isDeferred } from './module/functions-variables' 12 | 13 | import * as classes from './module/classes' 14 | 15 | // export * from './module/classes' 16 | export { default } from './module/classes' 17 | 18 | export type repo = classes.UserRepository['user'] 19 | export type UserC = typeof import('./module/classes').default 20 | 21 | import { Myspace, log, MyModule } from './module/namespaces' 22 | export { log as aliasedLog, Myspace as AliasedNamespace, MyModule as AliasedModule } 23 | 24 | export * from './module/enums' 25 | 26 | import classic = require('./module/export-equals') 27 | // export default classic 28 | export { classic } 29 | 30 | // import { DEFAULT_EXTENSIONS } from '@babel/core' 31 | // export default DEFAULT_EXTENSIONS 32 | 33 | // export default (arg: string) => { 34 | // return arg.toUpperCase() 35 | // } 36 | 37 | import { isNumber as yo } from './module/functions-variables' 38 | export { yo } 39 | // export default yo 40 | 41 | // export default function aa(arg: string) { 42 | // return arg.toUpperCase() 43 | // } 44 | 45 | import { World } from './module/interfaces' 46 | 47 | export type Greeting = { world: `hello ${World}` } 48 | -------------------------------------------------------------------------------- /__fixtures__/bundle/complex/module/classes.ts: -------------------------------------------------------------------------------- 1 | import { User, OtherInterface } from './interfaces' 2 | 3 | /** 4 | * @public 5 | */ 6 | export default class UserController { 7 | /** 8 | * User repository. 9 | */ 10 | repository: UserRepository 11 | 12 | /** 13 | * Creates an instance of UserController 14 | */ 15 | constructor(repository: UserRepository) { 16 | this.repository = repository 17 | } 18 | 19 | /** 20 | * @returns a user 21 | */ 22 | getOne(): User { 23 | return { 24 | name: this.repository.names[random(0, 3)], 25 | age: random(20, 50), 26 | gender: 'M', 27 | } 28 | } 29 | } 30 | 31 | const globalThis = 'user' 32 | 33 | /** 34 | * @public 35 | */ 36 | export class UserRepository { 37 | /** 38 | * Model name 39 | */ 40 | static model = globalThis 41 | 42 | names = ['Jeremy', 'Sophie', 'Damien', 'Laetitia'] 43 | } 44 | 45 | // tslint:disable-next-line: no-empty-interface 46 | export interface UserRepository extends OtherInterface {} 47 | 48 | export namespace UserRepository { 49 | export function getModel() { 50 | return UserRepository.model 51 | } 52 | } 53 | 54 | function random(min: number, max: number) { 55 | return Math.floor(Math.random() * (max - min + 1)) + min 56 | } 57 | -------------------------------------------------------------------------------- /__fixtures__/bundle/complex/module/enums.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Status codes 3 | */ 4 | export enum Status { 5 | OK = 200, 6 | Error = 400, 7 | } 8 | 9 | export namespace Status { 10 | export function getError() { 11 | return Status.Error 12 | } 13 | } 14 | 15 | /** 16 | * content-type 17 | */ 18 | export enum ContentType { 19 | Json = 'application/json', 20 | Html = 'text/html', 21 | } 22 | 23 | /** 24 | * HTTP verbs 25 | */ 26 | export const enum Verb { 27 | Get = 'GET', 28 | Post = 'POST', 29 | } 30 | -------------------------------------------------------------------------------- /__fixtures__/bundle/complex/module/export-equals.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @public 3 | */ 4 | const fooBarBaz = { foo: 1, bar: 2, baz: 3, fooBarBaz: 123 } as const 5 | export = fooBarBaz 6 | 7 | // export = { foo: 1, bar: 2, baz: 3 } 8 | -------------------------------------------------------------------------------- /__fixtures__/bundle/complex/module/external.ts: -------------------------------------------------------------------------------- 1 | import ts from 'typescript' 2 | const emitFlags = ts.EmitFlags 3 | export { emitFlags } 4 | 5 | import typescript = require('typescript') 6 | export const options: typescript.TokenFlags = 1 7 | 8 | export { run } from 'jest' 9 | export { IncomingHttpHeaders } from 'http' 10 | 11 | export * from 'tslint' 12 | export * from 'dns' 13 | 14 | import * as utils from 'util' 15 | export { utils } 16 | 17 | import { StringDecoder } from 'string_decoder' 18 | export { StringDecoder } 19 | 20 | import { OutgoingHttpHeaders } from 'http' 21 | export type OutgoingHeaders = OutgoingHttpHeaders 22 | 23 | export type TestScheduler = typeof import('jest').TestScheduler 24 | 25 | export { randomBytesB } from './namespaces' 26 | -------------------------------------------------------------------------------- /__fixtures__/bundle/complex/module/functions-variables.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable: prefer-const no-var-keyword one-variable-per-declaration only-arrow-functions 2 | import { User } from './interfaces' 3 | 4 | /** 5 | * @internal 6 | */ 7 | const isFunction = (obj: any) => { 8 | return typeof obj === 'function' 9 | } 10 | 11 | /** 12 | * @internal 13 | */ 14 | function isObject(obj: any) { 15 | return typeof obj === 'object' 16 | } 17 | 18 | /** 19 | * Checks if given object is Promise-like. 20 | * @deprecated use isAsync 21 | */ 22 | export function isPromise(obj: any): obj is Promise { 23 | return obj != null && isObject(obj) && isFunction(obj.then) 24 | } 25 | 26 | export { isPromise as isAsync } 27 | 28 | type KV = { 29 | first: 'first' 30 | [key: string]: string 31 | } 32 | 33 | /** 34 | * Checks if given object is Observable-like. 35 | * @see https://github.com/ReactiveX/rxjs/blob/master/src/internal/util/isObservable.ts 36 | * @internal 37 | */ 38 | export const isObservable: (obj: KV) => boolean = (obj) => { 39 | return !!obj && isFunction(obj.lift) && isFunction(obj.subscribe) 40 | } 41 | 42 | export const isObs = isObservable 43 | 44 | /** 45 | * @internal 46 | */ 47 | const isNumber = function(obj: any) { 48 | return typeof obj === 'number' 49 | } 50 | 51 | /** 52 | * @public 53 | */ 54 | function getUser(firstname: string | User): User { 55 | return { 56 | name: typeof firstname === 'string' ? firstname : firstname.name, 57 | age: 1, 58 | gender: 'F', 59 | } 60 | } 61 | 62 | export { isNumber, getUser } 63 | 64 | /** 65 | * Best number in the world 66 | * @beta 67 | */ 68 | let three = 3, 69 | four = () => 4, 70 | five = 5 71 | 72 | export { three, four as quatro, four as number4 } 73 | 74 | /** 75 | * Best name in the world. 76 | * It' true. 77 | * @alpha 78 | */ 79 | export var name = 'Jeremy' 80 | export type name = 'Jeremy' 81 | 82 | /** 83 | * @decorator 84 | * @public 85 | */ 86 | export function OverloadedDecorator(...args: Parameters): void 87 | 88 | export function OverloadedDecorator(): ParameterDecorator 89 | 90 | export function OverloadedDecorator(): ParameterDecorator { 91 | return (target, methodKey, index) => undefined 92 | } 93 | 94 | export { jsonFixture as fixture } from '../data.json' 95 | // import { jsonFixture as fixture } from '../data.json' 96 | // export { fixture } 97 | import * as jsonFile from '../data.json' 98 | export { jsonFile } 99 | -------------------------------------------------------------------------------- /__fixtures__/bundle/complex/module/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { KV } from './namespaces' 2 | import { ExternalModuleReference } from 'typescript' 3 | 4 | /** 5 | * Defines a JS class type. 6 | * @typeParam T - type of the instance 7 | * @public 8 | */ 9 | export type ClassType = new (...args: any[]) => T 10 | 11 | /** 12 | * Defines any type of decorator. 13 | * @public 14 | */ 15 | export type GenericDecorator = ( 16 | target: object, 17 | propertyKey?: string | symbol, 18 | descriptorOrIndex?: TypedPropertyDescriptor | number 19 | ) => any 20 | 21 | export interface PromiseLike { 22 | promise: Promise 23 | } 24 | 25 | type Promise = { then: (value: any) => T; catch: (reason: any) => void } 26 | 27 | /** 28 | * User model. 29 | * @public 30 | */ 31 | export interface User { 32 | /** 33 | * Name of the user. 34 | */ 35 | name: string 36 | 37 | /** 38 | * Age of the user. 39 | */ 40 | age: number 41 | 42 | /** 43 | * Gender of the user. 44 | * @defaultValue 'M' 45 | */ 46 | gender?: 'M' | 'F' 47 | 48 | keyvalue?: KV 49 | 50 | extern?: External 51 | } 52 | 53 | type External = ExternalModuleReference 54 | 55 | // export type KV = { [key: string]: any } 56 | 57 | export interface OtherInterface { 58 | other: string 59 | user: User 60 | } 61 | 62 | export type World = 'world' 63 | -------------------------------------------------------------------------------- /__fixtures__/bundle/complex/module/namespaces.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from 'crypto' 2 | export { randomBytes as randomBytesB } 3 | 4 | /** 5 | * public 6 | */ 7 | export const log: Myspace.logger = (message) => console.log(message) 8 | 9 | export type KV = { [k: number]: any } 10 | 11 | /** 12 | * Container namespace 13 | */ 14 | export namespace Myspace { 15 | /** 16 | * @beta 17 | */ 18 | export type logger = (message: any) => void 19 | 20 | /** 21 | * @alpha 22 | */ 23 | export const random = randomBytes(32) 24 | } 25 | 26 | /** 27 | * My module 28 | * @alpha 29 | */ 30 | // tslint:disable-next-line: no-internal-module 31 | export module MyModule { 32 | export const obj = { yo: 2 } 33 | } 34 | -------------------------------------------------------------------------------- /__fixtures__/bundle/duplicate/main.ts: -------------------------------------------------------------------------------- 1 | /** X */ 2 | export class X {} 3 | export { X as Y } 4 | 5 | /** A */ 6 | class A {} 7 | export { A as B } 8 | export { A as C } 9 | 10 | export default A 11 | -------------------------------------------------------------------------------- /__fixtures__/bundle/external-name-conflict/interface.ts: -------------------------------------------------------------------------------- 1 | import { PathLike } from 'fs' 2 | 3 | export interface I { 4 | path: globalThis.Partial 5 | } 6 | -------------------------------------------------------------------------------- /__fixtures__/bundle/external-name-conflict/main.ts: -------------------------------------------------------------------------------- 1 | import { I } from './interface' 2 | 3 | export class PathLike { 4 | path!: I 5 | } 6 | -------------------------------------------------------------------------------- /__fixtures__/bundle/global-name-conflict/get-real-date.ts: -------------------------------------------------------------------------------- 1 | export function getRealDate(): Date { 2 | return new Date() 3 | } 4 | -------------------------------------------------------------------------------- /__fixtures__/bundle/global-name-conflict/main.ts: -------------------------------------------------------------------------------- 1 | export { isMyPromise } from './my-promise' 2 | 3 | export { getRealDate } from './get-real-date' 4 | 5 | export { Date } from './my-date' 6 | 7 | import { Date as MyDate } from './my-date' 8 | 9 | export const myDate = new MyDate() 10 | 11 | export const Date_1 = new Date().getTime() // tslint:disable-line: variable-name 12 | -------------------------------------------------------------------------------- /__fixtures__/bundle/global-name-conflict/my-date.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Not the global Date. 3 | */ 4 | export class Date {} 5 | -------------------------------------------------------------------------------- /__fixtures__/bundle/global-name-conflict/my-promise.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Not the global Promise. 3 | */ 4 | export type Promise = { then: (value: any) => any; catch: (reason: any) => void } 5 | 6 | export function isMyPromise(obj: any): obj is Promise { 7 | return true 8 | } 9 | -------------------------------------------------------------------------------- /__fixtures__/bundle/namespace-merge/interfaces.ts: -------------------------------------------------------------------------------- 1 | export type StatusCode = 2 | | StatusCode.Information 3 | | StatusCode.Success 4 | | StatusCode.Redirection 5 | | StatusCode.ClientError 6 | | StatusCode.ServerError 7 | 8 | // Both need to be exported to avoid false positive of name conflict. 9 | 10 | export namespace StatusCode { 11 | export type Information = 100 | 101 | 102 | 103 12 | export type Success = 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226 13 | export type Redirection = 300 | 301 | 302 | 303 | 304 | 307 | 308 14 | // prettier-ignore 15 | export type ClientError = 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451 16 | export type ServerError = 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511 17 | } 18 | -------------------------------------------------------------------------------- /__fixtures__/bundle/namespace-merge/main.ts: -------------------------------------------------------------------------------- 1 | import { StatusCode } from './interfaces' 2 | 3 | type Request = { 4 | status: StatusCode 5 | } 6 | 7 | export const request: Request = { status: 100 } 8 | -------------------------------------------------------------------------------- /__fixtures__/bundle/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "lib": ["es2017"], 6 | "outDir": "dist", 7 | "strict": true, 8 | "declaration": true, 9 | "newLine": "LF", 10 | "moduleResolution": "node", 11 | "esModuleInterop": true, 12 | "resolveJsonModule": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /__tests__/builder.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { existsSync } from 'fs' 3 | import { createProgramFromConfig, build } from '../src' 4 | import { rmrf } from '../src/utils/fs' 5 | 6 | const basePath = join(__dirname, '..', '__fixtures__', 'basic') 7 | 8 | const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation() 9 | const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation() 10 | 11 | afterEach(() => { 12 | consoleLogSpy.mockClear() 13 | consoleWarnSpy.mockClear() 14 | 15 | rmrf(join(basePath, 'dist')) 16 | }) 17 | 18 | afterAll(() => { 19 | consoleWarnSpy.mockRestore() 20 | consoleLogSpy.mockRestore() 21 | }) 22 | 23 | test('Create program by overriding config file', async () => { 24 | const program = createProgramFromConfig({ 25 | basePath, 26 | configFilePath: 'tsconfig.json', 27 | compilerOptions: { 28 | rootDir: 'src', 29 | outDir: 'dist', 30 | declaration: 'true' as any, 31 | skipLibCheck: true, 32 | }, 33 | exclude: ['**/excluded', '**/dist'], 34 | }) 35 | 36 | expect(program.getCompilerOptions()).toMatchObject({ 37 | strict: true, 38 | // `compilerOptions` properties returns unix separators in windows paths 39 | rootDir: join(basePath, 'src').replace(/\\/g, '/'), 40 | declaration: undefined, 41 | }) 42 | 43 | expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining("'declaration' requires a value of type boolean")) 44 | 45 | expect(program.getRootFileNames()).toHaveLength(2) 46 | }) 47 | 48 | test('Build without errors with config from scratch', async () => { 49 | build({ 50 | basePath, 51 | compilerOptions: { 52 | module: 'commonjs', 53 | moduleResolution: 'node', 54 | target: 'es2019', 55 | lib: ['es2019'], 56 | rootDir: 'src', 57 | outDir: 'dist', 58 | resolveJsonModule: true, 59 | // listEmittedFiles: true, 60 | esModuleInterop: true, 61 | listFiles: true, 62 | declaration: true, 63 | skipLibCheck: true, 64 | }, 65 | }) 66 | 67 | expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Files to compile')) // listFiles: true 68 | expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('successful')) 69 | 70 | expect(consoleWarnSpy).not.toHaveBeenCalledWith(expect.stringContaining('error')) 71 | 72 | const distMainFile = join(basePath, 'dist', 'main.js') 73 | expect(existsSync(distMainFile)).toBe(true) 74 | }) 75 | -------------------------------------------------------------------------------- /__tests__/bundle-addon.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs' 2 | import { join } from 'path' 3 | import { build, TsConfigCompilerOptions } from '../src' 4 | 5 | const fixture = (...path: string[]) => join(__dirname, '..', '__fixtures__', 'bundle', ...path) 6 | 7 | const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation() 8 | const log = jest.requireActual('console').log 9 | 10 | afterEach(() => { 11 | consoleLogSpy.mockClear() 12 | }) 13 | 14 | afterAll(() => { 15 | consoleLogSpy.mockRestore() 16 | }) 17 | 18 | const compilerOptions: TsConfigCompilerOptions = { rootDir: '.', outDir: 'dist' } 19 | 20 | test('duplicate', () => { 21 | const entryPoint = fixture('duplicate', 'dist', 'main.d.ts') 22 | 23 | build({ 24 | basePath: fixture('duplicate'), 25 | extends: '../tsconfig.json', 26 | compilerOptions, 27 | clean: { outDir: true }, 28 | bundleDeclaration: { 29 | entryPoint, 30 | }, 31 | }) 32 | 33 | const bundled = readFileSync(entryPoint, 'utf8') 34 | 35 | for (const expected of [ 36 | 'export declare class X', 37 | 'export { X as Y }', 38 | 'export declare class B', 39 | 'export declare class C', 40 | 'export default B', 41 | ]) { 42 | expect(bundled).toContain(expected) 43 | } 44 | 45 | expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('successful')) 46 | }) 47 | 48 | test('global name conflict', () => { 49 | const entryPoint = fixture('global-name-conflict', 'dist', 'main.d.ts') 50 | 51 | build({ 52 | basePath: fixture('global-name-conflict'), 53 | extends: '../tsconfig.json', 54 | compilerOptions, 55 | clean: { outDir: true }, 56 | bundleDeclaration: { 57 | entryPoint, 58 | }, 59 | }) 60 | 61 | const bundled = readFileSync(entryPoint, 'utf8') 62 | 63 | for (const expected of [ 64 | /^type Promise_1 =/gm, 65 | /^export declare function isMyPromise.*Promise_1/gm, 66 | /^export declare function getRealDate.*Date;$/gm, 67 | /^declare class Date_2/gm, 68 | /^export { Date_2 as Date }/gm, 69 | /^export declare const myDate: Date_2/gm, 70 | ]) { 71 | expect(bundled).toMatch(expected) 72 | } 73 | 74 | expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('successful')) 75 | }) 76 | 77 | test.skip('external name conflict', () => { 78 | const entryPoint = fixture('external-name-conflict', 'dist', 'main.d.ts') 79 | 80 | build({ 81 | basePath: fixture('external-name-conflict'), 82 | extends: '../tsconfig.json', 83 | compilerOptions, 84 | clean: { outDir: true }, 85 | bundleDeclaration: { 86 | entryPoint, 87 | }, 88 | }) 89 | 90 | const bundled = readFileSync(entryPoint, 'utf8') 91 | log('bundled', bundled) 92 | // todo: conflict exists but is not detected during build. 93 | 94 | // expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('successful')) 95 | }) 96 | 97 | test('ambient declaration', () => { 98 | const entryPoint = fixture('ambient-declaration', 'dist', 'main.d.ts') 99 | 100 | build({ 101 | basePath: fixture('ambient-declaration'), 102 | extends: '../tsconfig.json', 103 | compilerOptions, 104 | clean: { outDir: true }, 105 | bundleDeclaration: { 106 | entryPoint, 107 | }, 108 | }) 109 | 110 | const bundled = readFileSync(entryPoint, 'utf8') 111 | 112 | for (const expected of [ 113 | /^declare global {.*interface GlobalInterface {$/ms, 114 | /^declare global {.*namespace GlobalNamespace {$/ms, 115 | /^declare module 'os' {$/m, 116 | /^declare module 'http' {$/m, 117 | /^import { ExternalInterface } from "http";$/m, 118 | ]) { 119 | expect(bundled).toMatch(expected) 120 | } 121 | 122 | expect(bundled.match(/namespace GlobalNamespace {$/gm)).toHaveLength(2) 123 | expect(bundled.match(/interface InternalInterface {$/gm)).toHaveLength(2) 124 | 125 | expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('successful')) 126 | }) 127 | 128 | test('circular reference bug', () => { 129 | const entryPoint = fixture('circular-reference-bug', 'dist', 'main.d.ts') 130 | 131 | build({ 132 | basePath: fixture('circular-reference-bug'), 133 | extends: '../tsconfig.json', 134 | compilerOptions, 135 | clean: { outDir: true }, 136 | bundleDeclaration: { 137 | entryPoint, 138 | }, 139 | }) 140 | 141 | const bundled = readFileSync(entryPoint, 'utf8') 142 | expect(bundled).toMatch(/^export declare class User {$/m) 143 | 144 | expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('successful')) 145 | }) 146 | 147 | test('namespace merge', () => { 148 | const entryPoint = fixture('namespace-merge', 'dist', 'main.d.ts') 149 | 150 | build({ 151 | basePath: fixture('namespace-merge'), 152 | extends: '../tsconfig.json', 153 | compilerOptions, 154 | clean: { outDir: true }, 155 | bundleDeclaration: { 156 | entryPoint, 157 | }, 158 | }) 159 | 160 | const bundled = readFileSync(entryPoint, 'utf8') 161 | 162 | expect(bundled).toMatch(/^type StatusCode = StatusCode\./m) 163 | expect(bundled).toMatch(/^declare namespace StatusCode {$/m) 164 | 165 | expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('successful')) 166 | }) 167 | 168 | test.skip('complex', () => { 169 | const entryPoint = fixture('complex', 'dist', 'main.d.ts') 170 | 171 | build({ 172 | basePath: fixture('complex'), 173 | extends: '../tsconfig.json', 174 | compilerOptions, 175 | bundleDeclaration: { 176 | entryPoint, 177 | }, 178 | }) 179 | 180 | const bundled = readFileSync(entryPoint, 'utf8') 181 | log(bundled) 182 | 183 | expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('successful')) 184 | }) 185 | -------------------------------------------------------------------------------- /__tests__/clean-addon.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { build } from '../src' 3 | 4 | const basePath = join(__dirname, '..', '__fixtures__', 'basic') 5 | 6 | // Mock deleting folders for protection 7 | jest.mock('../src/utils/fs', () => ({ 8 | ...jest.requireActual('../src/utils/fs'), 9 | rmrf: jest.fn((path) => console.info('mock rmrf on', path)), 10 | })) 11 | 12 | const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation() 13 | 14 | afterEach(() => { 15 | consoleLogSpy.mockClear() 16 | 17 | // Need actual delete implementation to clean between some tests. 18 | // Can't unmock implicit imports on a per test basis https://github.com/facebook/jest/issues/2649 19 | const { rmrf } = jest.requireActual('../src/utils/fs') 20 | const outDirPaths = [join(basePath, 'dist'), join(basePath, 'src', 'dist')] 21 | outDirPaths.forEach(rmrf) 22 | }) 23 | 24 | afterAll(() => { 25 | consoleLogSpy.mockRestore() 26 | }) 27 | 28 | describe('Clean protections', () => { 29 | test('Forbid cleaning rootDir', async () => { 30 | expect(() => { 31 | build({ 32 | basePath, 33 | configFilePath: 'tsconfig.json', 34 | compilerOptions: { rootDir: 'src' }, 35 | clean: ['src'], 36 | }) 37 | }).toThrow('cannot delete') 38 | }) 39 | 40 | test('Forbid cleaning basePath and up', async () => { 41 | expect(() => { 42 | build({ 43 | basePath, 44 | configFilePath: 'tsconfig.json', 45 | clean: ['.'], 46 | }) 47 | }).toThrow('cannot delete') 48 | 49 | expect(() => { 50 | build({ 51 | basePath, 52 | configFilePath: 'tsconfig.json', 53 | clean: ['..'], 54 | }) 55 | }).toThrow('cannot delete') 56 | }) 57 | 58 | test('Forbid cleaning cwd', async () => { 59 | expect(() => { 60 | build({ 61 | basePath, 62 | configFilePath: 'tsconfig.json', 63 | clean: [process.cwd()], 64 | }) 65 | }).toThrow('cannot delete') 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /__tests__/copy-addon.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readdirSync } from 'fs' 2 | import { join } from 'path' 3 | import { build } from '../src' 4 | import { rmrf } from '../src/utils/fs' 5 | 6 | const basePath = join(__dirname, '..', '__fixtures__', 'basic-with-other') 7 | 8 | const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation() 9 | const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation() 10 | 11 | afterEach(() => { 12 | consoleLogSpy.mockClear() 13 | consoleWarnSpy.mockClear() 14 | 15 | rmrf(join(basePath, 'dist')) 16 | rmrf(join(basePath, 'src', 'dist')) 17 | }) 18 | 19 | afterAll(() => { 20 | consoleWarnSpy.mockRestore() 21 | consoleLogSpy.mockRestore() 22 | }) 23 | 24 | describe('Copy addon', () => { 25 | test('all other files', () => { 26 | const expectedOtherFiles = readdirSync(join(basePath, 'src', 'other')) 27 | 28 | build({ 29 | basePath, 30 | configFilePath: 'tsconfig.json', 31 | copyOtherToOutDir: true, 32 | exclude: ['**/excluded'], 33 | }) 34 | 35 | const otherDirDistPath = join(basePath, 'dist', 'other') 36 | expect(readdirSync(otherDirDistPath)).toHaveLength(expectedOtherFiles.length) 37 | 38 | const excludedDirDistPath = join(basePath, 'dist', 'excluded') 39 | expect(existsSync(excludedDirDistPath)).toBe(false) 40 | 41 | expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringMatching(/already.*main\.js/)) 42 | 43 | expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('successful')) 44 | }) 45 | 46 | test('do not recursively copy outDir to outDir', () => { 47 | // tslint:disable-next-line: no-shadowed-variable 48 | const basePath = join(__dirname, '..', '__fixtures__', 'basic-with-other', 'src') 49 | 50 | build({ 51 | basePath, 52 | configFilePath: '../tsconfig.json', 53 | copyOtherToOutDir: true, 54 | compilerOptions: { 55 | rootDir: '.', 56 | outDir: 'dist', 57 | }, 58 | exclude: ['**/excluded'], 59 | }) 60 | 61 | const distInDist = join(basePath, 'dist', 'dist') 62 | expect(existsSync(distInDist)).toBe(false) 63 | 64 | expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('successful')) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /build.ts: -------------------------------------------------------------------------------- 1 | // Nice contortion, bitch. 2 | 3 | import { build } from './src' 4 | 5 | build({ 6 | basePath: __dirname, 7 | extends: './tsconfig.json', 8 | compilerOptions: { 9 | rootDir: './src', 10 | outDir: './dist', 11 | skipLibCheck: true, 12 | }, 13 | include: ['src/**/*'], 14 | exclude: ['**/__tests__', '**/*.test.ts', '**/*.spec.ts', '**/__fixtures__'], 15 | clean: { outDir: true }, 16 | bundleDeclaration: { 17 | entryPoint: 'index.d.ts', 18 | }, 19 | }) 20 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@jest/types').Config.InitialOptions} */ 2 | const config = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | roots: ['/src/', '/__tests__/'], 6 | testPathIgnorePatterns: ['/node_modules/', '/__fixtures__/', '.*\\.d\\.ts$'], 7 | globals: { 8 | 'ts-jest': { 9 | diagnostics: { 10 | // https://kulshekhar.github.io/ts-jest/user/config/diagnostics 11 | warnOnly: true, 12 | pretty: false, 13 | }, 14 | }, 15 | }, 16 | } 17 | 18 | module.exports = config 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsc-prog", 3 | "version": "2.3.0", 4 | "author": "Jeremy Bensimon", 5 | "license": "MIT", 6 | "repository": "github:jeremyben/tsc-prog", 7 | "keywords": [ 8 | "typescript", 9 | "tsc", 10 | "compiler", 11 | "programmatic" 12 | ], 13 | "main": "dist/index.js", 14 | "types": "dist/index.d.ts", 15 | "files": [ 16 | "dist" 17 | ], 18 | "engines": { 19 | "node": ">=12" 20 | }, 21 | "engineStrict": true, 22 | "scripts": { 23 | "build": "ts-node -T build.ts", 24 | "prepublishOnly": "yarn build", 25 | "release": "standard-version", 26 | "test": "jest --runInBand", 27 | "test:file": "jest --runInBand --testPathPattern", 28 | "test:watch": "jest --runInBand --watch --verbose false" 29 | }, 30 | "dependencies": {}, 31 | "peerDependencies": { 32 | "typescript": ">=4" 33 | }, 34 | "devDependencies": { 35 | "@types/jest": "^27.0.2", 36 | "@types/node": "^16.10.1", 37 | "jest": "^27.2.2", 38 | "standard-version": "^9.3.1", 39 | "ts-jest": "^27.0.5", 40 | "ts-node": "^10.2.1", 41 | "typescript": "^4.9.5" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/builder.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import { CreateProgramFromConfigOptions, TsConfig, EmitOptions, BuildOptions } from './interfaces' 3 | import { ensureAbsolutePath } from './utils/path' 4 | import { logDiagnostics, Color } from './utils/log' 5 | import cleanTargets, { protectSensitiveFolders } from './clean-addon' 6 | import copyOtherFiles, { excludeKey } from './copy-addon' 7 | import { bundleDts, getDtsInterceptor } from './bundle-addon' 8 | 9 | /** 10 | * Compiles .ts files by creating a compilation object with the compiler API and emitting .js files. 11 | * @public 12 | */ 13 | export function build(options: BuildOptions) { 14 | const program = createProgramFromConfig(options) 15 | emit(program, options) 16 | } 17 | 18 | /** 19 | * Creates a compilation object using the compiler API. 20 | * @public 21 | */ 22 | export function createProgramFromConfig({ 23 | basePath, 24 | configFilePath, 25 | compilerOptions, 26 | include, 27 | exclude, 28 | files, 29 | extends: extend, // cuz keyword 30 | references, 31 | host, 32 | }: CreateProgramFromConfigOptions) { 33 | let config: TsConfig = {} 34 | 35 | if (configFilePath) { 36 | configFilePath = ensureAbsolutePath(configFilePath, basePath) 37 | console.log(`Retrieving ${configFilePath}`) 38 | 39 | const readResult = ts.readConfigFile(configFilePath, ts.sys.readFile) 40 | 41 | if (readResult.error) { 42 | const isTTY = !!ts.sys.writeOutputIsTTY && ts.sys.writeOutputIsTTY() 43 | logDiagnostics([readResult.error], isTTY) 44 | } 45 | 46 | config = readResult.config 47 | } 48 | 49 | config.compilerOptions = Object.assign({}, config.compilerOptions, compilerOptions) 50 | if (include) config.include = include 51 | if (exclude) config.exclude = exclude 52 | if (files) config.files = files 53 | if (extend) config.extends = extend 54 | if (references) config.references = references 55 | 56 | const { options, fileNames, projectReferences, errors } = ts.parseJsonConfigFileContent( 57 | config, 58 | ts.sys, 59 | basePath, 60 | undefined, 61 | configFilePath 62 | ) 63 | 64 | if (errors && errors.length) { 65 | const isTTY = !!ts.sys.writeOutputIsTTY && ts.sys.writeOutputIsTTY() 66 | logDiagnostics(errors, isTTY) 67 | } 68 | 69 | const program = ts.createProgram({ 70 | options, 71 | rootNames: fileNames, 72 | projectReferences, 73 | host, 74 | }) 75 | 76 | // https://github.com/Microsoft/TypeScript/issues/1863 77 | ;(program as any)[excludeKey] = config.exclude 78 | 79 | return program 80 | } 81 | 82 | /** 83 | * Compiles TypeScript files and emits diagnostics if any. 84 | * @public 85 | */ 86 | export function emit(program: ts.Program, { basePath, clean, copyOtherToOutDir, bundleDeclaration }: EmitOptions = {}) { 87 | const options = program.getCompilerOptions() 88 | 89 | if (copyOtherToOutDir && !options.outDir) { 90 | throw Color.red('Cannot copy: you must define `outDir` in the compiler options') 91 | } 92 | 93 | // Write .d.ts files to an in memory Map in case of bundling. 94 | const dtsCache = new Map() 95 | let dtsInterceptor: ts.WriteFileCallback | undefined 96 | 97 | if (bundleDeclaration) { 98 | if (!options.declaration) { 99 | throw Color.red('Cannot bundle declarations: you must turn `declaration` on in the compiler options') 100 | } 101 | if (options.declarationMap) { 102 | console.warn(Color.yellow("`declarationMap` won't work with declaration bundling")) 103 | } 104 | 105 | dtsInterceptor = getDtsInterceptor(dtsCache) 106 | } 107 | 108 | if (clean) { 109 | let targets: string[] = [] 110 | 111 | if (Array.isArray(clean)) { 112 | targets = clean.map((t) => ensureAbsolutePath(t, basePath)) 113 | } else { 114 | if (clean.outDir && options.outDir) targets.push(options.outDir) 115 | if (clean.outFile && options.outFile) targets.push(options.outFile) 116 | if (clean.declarationDir && options.declarationDir) targets.push(options.declarationDir) 117 | } 118 | 119 | protectSensitiveFolders(targets, options.rootDir, basePath) 120 | cleanTargets(targets) 121 | } 122 | 123 | if (options.listFiles) { 124 | console.log('Files to compile:\n' + program.getRootFileNames().join('\n')) 125 | } 126 | 127 | console.log('Compilation started') 128 | // tslint:disable-next-line: prefer-const 129 | let { diagnostics, emitSkipped, emittedFiles } = program.emit(undefined, dtsInterceptor) 130 | 131 | if (options.listEmittedFiles && emittedFiles) { 132 | if (bundleDeclaration) { 133 | emittedFiles = emittedFiles.filter((path) => !dtsCache.has(path)) 134 | } 135 | console.log('Emitted files:\n' + emittedFiles.join('\n')) 136 | } 137 | 138 | // https://github.com/dsherret/ts-morph/issues/384 139 | const allDiagnostics = ts.getPreEmitDiagnostics(program).concat(diagnostics) 140 | logDiagnostics(allDiagnostics, options.pretty) 141 | 142 | if (!options.noEmit && emitSkipped) { 143 | throw Color.red('Compilation failed') 144 | } 145 | 146 | if (copyOtherToOutDir) { 147 | console.log('Copying other files to `outDir`') 148 | const copiedFiles = copyOtherFiles(program) 149 | 150 | if (options.listEmittedFiles) { 151 | console.log('Copied files:\n' + copiedFiles.join('\n')) 152 | } 153 | } 154 | 155 | if (bundleDeclaration) { 156 | console.log('Bundling declarations') 157 | bundleDts(program, dtsCache, bundleDeclaration) 158 | } 159 | 160 | if (allDiagnostics.length) { 161 | console.log(Color.yellow(`Compilation done with ${allDiagnostics.length} errors`)) 162 | } else { 163 | console.log(Color.green('Compilation successful')) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/bundle-addon/declaration-collector.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import { Color } from '../utils/log' 3 | import { pushDeep, MapEntry } from '../utils/manipulation' 4 | import { Reference, SymbolCollector } from './symbol-collector' 5 | import { DeclarationRegistrar, Replacement } from './declaration-registrar' 6 | import { isExternalLibraryAugmentation } from './syntax-check' 7 | import { 8 | getModuleSpecifier, 9 | getModuleNameFromSpecifier, 10 | getDeclarationName, 11 | lookForProperty, 12 | findFirstParent, 13 | } from './syntax-retrieval' 14 | 15 | /** 16 | * @internal 17 | */ 18 | export class DeclarationCollector { 19 | declarations: DeclarationRegistrar 20 | 21 | private symbols: SymbolCollector 22 | 23 | /** 24 | * Map of exports whose original symbol name conflicts with the global scope, 25 | * that must first imported as a different name before being re-exported. 26 | * 27 | * Example: Given a local declaration `declare class Date`, 28 | * we must write `declare class Date_1` & `export { Date_1 as Date }` to avoid conflicts. 29 | */ 30 | private exportRenames: Map 31 | 32 | /** 33 | * Map of collected references that could have the same name, associated with their symbols. 34 | */ 35 | private refsDeclared: Map 36 | 37 | private program: ts.Program 38 | private checker: ts.TypeChecker 39 | private entryFile: ts.SourceFile 40 | 41 | private declareGlobalsOption: boolean 42 | private declareAugmentationsOption: boolean 43 | 44 | private debugSwitch = false 45 | 46 | constructor( 47 | symbols: SymbolCollector, 48 | entryFile: ts.SourceFile, 49 | program: ts.Program, 50 | declareGlobalsOption = true, 51 | declareAugmentationsOption = true 52 | ) { 53 | this.program = program 54 | this.checker = program.getTypeChecker() 55 | this.entryFile = entryFile 56 | this.symbols = symbols 57 | this.declareGlobalsOption = declareGlobalsOption 58 | this.declareAugmentationsOption = declareAugmentationsOption 59 | this.refsDeclared = new Map() 60 | this.declarations = new DeclarationRegistrar() 61 | this.exportRenames = this.getExportRenames(this.symbols.exportSymbols) 62 | 63 | for (const moduleName of this.symbols.externalStarModuleNames) { 64 | this.declarations.registerStar(moduleName) 65 | } 66 | 67 | for (const exportSymbol of this.symbols.exportSymbols) { 68 | this.collectExports(exportSymbol) 69 | } 70 | 71 | if (this.declareGlobalsOption) { 72 | for (const internalGlobalSymbol of this.symbols.internalGlobalSymbols) { 73 | this.debug('internal-global', internalGlobalSymbol.name) 74 | this.declarations.registerGlobal(internalGlobalSymbol) 75 | } 76 | } 77 | } 78 | 79 | private getExportRenames(exportSymbols: SymbolCollector['exportSymbols']) { 80 | const exportRenames = new Map() 81 | 82 | exportSymbols.forEach(([origSymbol], exportSymbol) => { 83 | const exportName = exportSymbol.escapedName 84 | let rename: string | undefined 85 | 86 | // Don't overwrite symbols already renamed. 87 | if (exportRenames.has(origSymbol)) { 88 | return 89 | } 90 | 91 | if (this.nameIsAlreadyGlobal(exportName)) { 92 | let suffix = 1 93 | rename = `${exportName}_${suffix}` 94 | 95 | while (this.nameIsAlreadyExportedOrGlobal(rename)) { 96 | rename = `${exportName}_${++suffix}` 97 | } 98 | } 99 | 100 | if (rename) { 101 | this.debug('export', 'will-be-renamed-then-reexported:', exportName, rename) 102 | exportRenames.set(origSymbol, rename) 103 | } 104 | }) 105 | 106 | return exportRenames 107 | } 108 | 109 | private collectExports([exportSymbol, [origSymbol, references]]: MapEntry): void { 110 | // We look in the first declaration to retrieve common source file, 111 | // since merged and overloaded declarations are in the same source file. 112 | const sourceFile = origSymbol.declarations![0].getSourceFile() 113 | 114 | const exportName = exportSymbol.escapedName 115 | const origName = origSymbol.escapedName 116 | // console.log(exportSymbol.declarations.map((d) => d.getFullText())) 117 | // console.log(origSymbol.declarations.map((d) => d.getFullText())) 118 | 119 | // 120 | // ═════════ External/JSON ═════════ 121 | // 122 | 123 | if (this.program.isSourceFileFromExternalLibrary(sourceFile)) { 124 | // External declarations exported directly without intermediary symbol in our package, 125 | // usually via a star export. Should not happen since we chose before to not retrieve them. 126 | if (exportSymbol === origSymbol) { 127 | console.warn( 128 | Color.yellow(`Start exported symbols from "${sourceFile.fileName}" should not have been retrieved.`) 129 | ) 130 | return 131 | } 132 | this.debug('export', 'is-from-external-lib:', exportName) 133 | this.handleSymbolFromExternalLibrary(exportSymbol, true) 134 | return 135 | } 136 | 137 | if (ts.isJsonSourceFile(sourceFile)) { 138 | this.debug('export', 'is-from-json-file:', exportName) 139 | this.handleSymbolFromJsonFile(exportSymbol, sourceFile, true) 140 | return 141 | } 142 | 143 | // External module augmentations are not detected as external, and would be duplicated. 144 | if (this.shouldHandleExternalAugmentation(origSymbol.declarations![0])) { 145 | this.debug('export', 'is-augmentation-of-external-lib:', exportName) 146 | this.handleSymbolFromExternalLibrary(exportSymbol, true) 147 | return 148 | } 149 | 150 | // 151 | // ═════════ Internal ═════════ 152 | // 153 | 154 | // First register referenced declarations and retrieve replacements to the exported declaration if any. 155 | const symbolReplacements = this.collectReferences(references) 156 | // console.log(`replacements for ${exportName}:`, symbolReplacements) 157 | 158 | // ─── `export default` ─── 159 | if (exportName === ts.InternalSymbolName.Default) { 160 | // Symbol is already exported, so we simply export the aliased name as default. 161 | const otherExportSymbols = this.findOtherExportsWithSameOrig(exportSymbol, origSymbol) 162 | if (otherExportSymbols) { 163 | // Export only one default. 164 | const aliasName = otherExportSymbols[0].escapedName 165 | this.debug('export', 'default-already-exported-to-alias:', aliasName) 166 | this.declarations.registerAlias(aliasName, { default: true }) 167 | return 168 | } 169 | 170 | // Default symbol is a variable value, and must be declared first before being exported as default. 171 | // If the value was exported as is, an intermediary variable named '_default' was created by the compiler. 172 | // todo: default namespace and type 173 | if (origSymbol.flags & ts.SymbolFlags.Variable) { 174 | const exportRealName = getDeclarationName(exportSymbol.declarations![0]) 175 | 176 | this.debug('export', 'default-variable-to-declare:', exportRealName || origName) 177 | this.declarations.registerInternal(origSymbol, { 178 | default: false, 179 | export: false, 180 | newName: exportRealName, 181 | symbolReplacements, 182 | }) 183 | this.declarations.registerAlias(exportRealName || origName, { default: true }) 184 | } else { 185 | this.debug('export', 'default-to-declare:', origName) 186 | this.declarations.registerInternal(origSymbol, { export: true, default: true, symbolReplacements }) 187 | } 188 | 189 | return 190 | } 191 | 192 | // Always remove `default` keyword from original declaration (origName === ts.InternalSymbolName.Default). 193 | // `export { default as A }` | (`import A` & `export { A }`) 194 | 195 | // ─── `export *` | `export { A }` ─── 196 | // Symbol is eiher directly exported or aliased with the same name. 197 | if (exportName === origName) { 198 | const exportRename = this.exportRenames.get(origSymbol) 199 | if (exportRename) { 200 | // Name is already used by a global symbol. 201 | this.debug('export', 'already-global:', exportName, '| to-declare-then-reexport-as:', exportRename) 202 | this.declarations.registerInternal(origSymbol, { 203 | export: false, 204 | default: false, 205 | newName: exportRename, 206 | symbolReplacements, 207 | }) 208 | this.declarations.registerAlias(exportRename as ts.__String, { as: exportName }) 209 | } else { 210 | // Usual case. 211 | this.debug('export', 'original-name-to-declare:', exportName) 212 | this.declarations.registerInternal(origSymbol, { export: true, default: false, symbolReplacements }) 213 | } 214 | 215 | // Symbol is also exported as different aliases. 216 | // + `export { A as B }` 217 | const otherExportSymbols = this.findOtherExportsWithSameOrig(exportSymbol, origSymbol) 218 | if (otherExportSymbols) { 219 | for (const other of otherExportSymbols) { 220 | this.debug('export', 'to-alias:', exportRename || exportName, 'as', other.escapedName) 221 | this.declarations.registerAlias((exportRename || exportName) as ts.__String, { as: other.escapedName }) 222 | } 223 | } 224 | 225 | // if (exportSymbol === origSymbol) `export *` // else `export { A }` 226 | return 227 | } 228 | 229 | // ─── `export { A as B }` ─── 230 | // Symbol is aliased as a different name than the original one. 231 | // Symbols aliased as a different name but also exported as the original name are handled in the previous condition. 232 | if (exportName !== origName && !this.nameIsAlreadyExported(origName)) { 233 | const exportRename = this.exportRenames.get(origSymbol) 234 | 235 | if (exportRename) { 236 | // Name is already used by a global symbol. 237 | this.debug('export', 'aliased-name-already-global:', exportName, '| to-declare-then-reexport:', exportRename) 238 | this.declarations.registerInternal(origSymbol, { 239 | export: false, 240 | default: false, 241 | newName: exportRename, 242 | symbolReplacements, 243 | }) 244 | this.declarations.registerAlias(exportRename as ts.__String, { as: exportName }) 245 | } else { 246 | // Usual case. 247 | this.debug('export', 'aliased-name-to-declare:', exportName) 248 | this.declarations.registerInternal(origSymbol, { 249 | export: true, 250 | default: false, 251 | newName: exportName, 252 | symbolReplacements, 253 | }) 254 | } 255 | 256 | return 257 | } 258 | } 259 | 260 | /** 261 | * Write references used by declarations and retrieves eventual replacements to rewrite parts of the declarations. 262 | */ 263 | private collectReferences(refs: Reference[]): Replacement[][] { 264 | const symbolReplacements: Replacement[][] = [] 265 | 266 | for (const { ref, subrefs, declarationIndex } of refs) { 267 | const refSymbol = this.checker.getSymbolAtLocation(ref) 268 | 269 | if (!refSymbol) { 270 | throw Error(`Cannot find symbol of reference: ${ref}`) 271 | } 272 | 273 | let origRefSymbol = refSymbol 274 | if (refSymbol.flags & ts.SymbolFlags.Alias) { 275 | origRefSymbol = this.checker.getAliasedSymbol(refSymbol) 276 | } 277 | 278 | const refName = refSymbol.escapedName 279 | const origRefName = origRefSymbol.escapedName 280 | 281 | // Don't handle generic type (e.g. `T`). 282 | if (origRefSymbol.flags & ts.SymbolFlags.TypeParameter) { 283 | continue 284 | } 285 | 286 | // 287 | // ═════════ Global ═════════ 288 | // 289 | 290 | if ( 291 | this.isGlobalSymbol(origRefSymbol) && 292 | // Redeclare internal global references if the global option is off. 293 | (this.declareGlobalsOption || (!this.declareGlobalsOption && !this.isInternalGlobalSymbol(origRefSymbol))) 294 | ) { 295 | this.debug('ref', 'is-global:', refName) 296 | continue 297 | } 298 | 299 | // 300 | // ═════════ Without declarations ═════════ 301 | // 302 | 303 | // Ignore symbols without declarations. 304 | // After globals, because some global symbols like `globalThis` does not have a declaration. 305 | if (!origRefSymbol.declarations || !origRefSymbol.declarations.length) { 306 | console.warn(Color.yellow(`Referenced symbol ${origRefSymbol.name} does not have any declaration`)) 307 | continue 308 | } 309 | 310 | // 311 | // ═════════ External/JSON ═════════ 312 | // 313 | // Don't have to rewrite external import type nodes: `import('lib').A`. 314 | 315 | const refSourceFile = origRefSymbol.declarations[0].getSourceFile() 316 | 317 | if (this.program.isSourceFileFromExternalLibrary(refSourceFile)) { 318 | if (ts.isImportTypeNode(ref)) continue 319 | // console.info(this.nameIsAlreadyExported(refName)) // todo: test external-name-conflict 320 | this.debug('ref', 'is-from-external-lib:', refName) 321 | this.handleSymbolFromExternalLibrary(refSymbol) 322 | continue 323 | } 324 | 325 | if (ts.isJsonSourceFile(refSourceFile)) { 326 | if (ts.isImportTypeNode(ref)) continue 327 | this.debug('ref', 'is-from-json-file:', refName) 328 | this.handleSymbolFromJsonFile(refSymbol, refSourceFile) 329 | continue 330 | } 331 | 332 | // External module augmentations are not detected as external, and would be duplicated. 333 | if (this.shouldHandleExternalAugmentation(origRefSymbol.declarations[0])) { 334 | if (ts.isImportTypeNode(ref)) continue 335 | this.debug('ref', 'is-augmentation-of-external-lib:', refName) 336 | this.handleSymbolFromExternalLibrary(refSymbol) 337 | continue 338 | } 339 | 340 | // 341 | // ═════════ Internal ═════════ 342 | // 343 | 344 | // ─── Already exported ─── 345 | if (this.isExportedSymbol(origRefSymbol)) { 346 | // Already exported as is. 347 | if (this.nameIsAlreadyExported(refName)) { 348 | // Export has possibly been renamed in its declaration if it conflicted with a global name. 349 | let possibleRename = this.exportRenames.get(origRefSymbol) 350 | if (possibleRename) { 351 | possibleRename = this.maybeTypeOf(possibleRename, ref) 352 | pushDeep(symbolReplacements, declarationIndex, { replace: ref, by: possibleRename }) 353 | } 354 | this.debug('ref', 'already-declared-and-exported:', possibleRename || refName) 355 | } 356 | // Already exported as an alias. 357 | else { 358 | let aliasName = 359 | this.exportRenames.get(origRefSymbol) || (this.findOtherAliasName(origRefSymbol, refName) as string) 360 | aliasName = this.maybeTypeOf(aliasName, ref) 361 | 362 | this.debug('ref', 'already-exported-as-alias:', aliasName) 363 | pushDeep(symbolReplacements, declarationIndex, { replace: ref, by: aliasName }) 364 | } 365 | continue 366 | } 367 | 368 | // First collect recursively sub references. 369 | const subSymbolReplacements = this.collectReferences(subrefs) 370 | 371 | // ─── Namespace reference ─── 372 | // Look for property and bundle it instead of whole namespace 373 | // in case of `import * as A` & `var x = A.a`, or `import('./A').a`. 374 | 375 | if (this.isImportedAsNamespaceSymbol(origRefSymbol)) { 376 | const match = lookForProperty(ref) 377 | 378 | if (!match) { 379 | throw Error(`Reference "${refName}" is aliasing the whole file ${origRefName} and thus can't be bundled.`) 380 | } 381 | 382 | // In `A.a`, the root is `A` and the prop is `a`. 383 | const [refRoot, refProp] = match 384 | 385 | let refPropOrigSymbol = this.checker.getSymbolAtLocation(refProp) 386 | if (!refPropOrigSymbol) { 387 | throw Error(`Can't find property symbol in namespace reference: ${ref}`) 388 | } 389 | 390 | if (refPropOrigSymbol.flags & ts.SymbolFlags.Alias) { 391 | refPropOrigSymbol = this.checker.getAliasedSymbol(refPropOrigSymbol) 392 | } 393 | 394 | // Retrieve the right name in case of default export. 395 | const refPropOrigName = 396 | refPropOrigSymbol.escapedName === ts.InternalSymbolName.Default 397 | ? getDeclarationName(refPropOrigSymbol.declarations![0]) 398 | : refPropOrigSymbol.escapedName 399 | 400 | if (!refPropOrigName || refPropOrigName === ts.InternalSymbolName.Default) { 401 | throw Error(`Unnamed property (probably default export) in namespace reference: ${ref}`) 402 | } 403 | 404 | // Property symbol already exported elsewhere from the entry file. 405 | if (this.isExportedSymbol(refPropOrigSymbol)) { 406 | // Exported as the same name. 407 | if (this.nameIsAlreadyExported(refPropOrigName)) { 408 | const possibleExportRename = this.exportRenames.get(refPropOrigSymbol) 409 | const newName = this.maybeTypeOf(possibleExportRename || refPropOrigName, ref) 410 | 411 | this.debug('ref', 'prop-already-exported-to-reuse:', newName) 412 | pushDeep(symbolReplacements, declarationIndex, { replace: refRoot, by: newName }) 413 | } 414 | // Exported as a different name, which we're looking for. 415 | else { 416 | let aliasName = this.findOtherAliasName(refPropOrigSymbol, refPropOrigName) as string 417 | aliasName = this.maybeTypeOf(aliasName, ref) 418 | 419 | this.debug('ref', 'prop-already-exported-as-alias-to-reuse:', aliasName) 420 | pushDeep(symbolReplacements, declarationIndex, { replace: refRoot, by: aliasName }) 421 | } 422 | } 423 | // Property symbol not exported, so we need to declare it. 424 | else { 425 | const newName = this.maybeTypeOf(refPropOrigName, ref) 426 | this.debug('ref', 'prop-to-declare:', newName) 427 | 428 | pushDeep(symbolReplacements, declarationIndex, { replace: refRoot, by: newName }) 429 | this.declarations.registerInternal(refPropOrigSymbol, { default: false, export: false }) 430 | } 431 | 432 | continue 433 | } 434 | 435 | if (this.nameIsAlreadyGlobal(refName)) { 436 | // We want suffix to start at 1, but indexes start at 0, so we make sure to add 1 each time. 437 | let suffix: number 438 | 439 | // Name has also already been declared by another reference. 440 | if (this.refsDeclared.has(refName)) { 441 | const sameNameSymbols = this.refsDeclared.get(refName)! 442 | const symbolIndex = sameNameSymbols.indexOf(origRefSymbol) 443 | 444 | // Symbol found, we use its index to build the suffix. 445 | if (symbolIndex >= 0) { 446 | suffix = symbolIndex + 1 447 | } 448 | 449 | // Symbol not declared before, so we assign a suffix and keep its reference. 450 | else { 451 | suffix = sameNameSymbols.push(origRefSymbol) 452 | this.refsDeclared.set(refName, sameNameSymbols) 453 | } 454 | } 455 | // First time a reference with this name is declared. 456 | else { 457 | suffix = 1 458 | this.refsDeclared.set(refName, [origRefSymbol]) 459 | } 460 | 461 | const newRefName = origRefName + '_' + suffix 462 | this.debug('ref', 'already-global:', refName, '| to-declare:', newRefName) 463 | 464 | pushDeep(symbolReplacements, declarationIndex, { replace: ref, by: newRefName }) 465 | this.declarations.registerInternal(origRefSymbol, { 466 | default: false, 467 | export: false, 468 | newName: newRefName, 469 | symbolReplacements: subSymbolReplacements, 470 | }) 471 | 472 | continue 473 | } 474 | 475 | // Name has already been declared by another reference. 476 | if (this.nameIsAlreadyUsebByRef(refName)) { 477 | let newRefName: string | undefined 478 | const sameNameSymbols = this.refsDeclared.get(refName)! 479 | const symbolIndex = sameNameSymbols.indexOf(origRefSymbol) 480 | 481 | // Symbol indexed at 0 is the first one declared, no need to suffix it. 482 | if (symbolIndex === 0) { 483 | this.debug('ref', 'to-reuse:', refName) 484 | } 485 | 486 | // Symbol not found, so it's another symbol with the same name, and we suffix it. 487 | if (symbolIndex === -1) { 488 | // Pick a suffix that would not make the new name conflict with another declaration. 489 | let suffix = sameNameSymbols.length 490 | newRefName = `${refName}_${suffix}` 491 | 492 | while (this.nameIsAlreadyExportedOrGlobal(newRefName)) { 493 | this.debug('ref', 'name-suffixed-would-conflict:', newRefName) 494 | newRefName = `${refName}_${++suffix}` 495 | } 496 | this.debug('ref', 'already-in-use:', refName, '| name-suffixed-to-declare:', newRefName) 497 | 498 | sameNameSymbols[suffix] = origRefSymbol 499 | this.refsDeclared.set(refName, sameNameSymbols) 500 | 501 | pushDeep(symbolReplacements, declarationIndex, { replace: ref, by: newRefName }) 502 | } 503 | 504 | // Symbol found other than the first one declared, we use its index as the suffix. 505 | if (symbolIndex > 0) { 506 | const suffix = symbolIndex 507 | 508 | newRefName = `${refName}_${suffix}` 509 | this.debug('ref', 'name-suffixed-to-reuse:', newRefName) 510 | 511 | pushDeep(symbolReplacements, declarationIndex, { replace: ref, by: newRefName }) 512 | } 513 | 514 | this.declarations.registerInternal(origRefSymbol, { 515 | default: false, 516 | export: false, 517 | newName: newRefName /* may be undefined */, 518 | symbolReplacements: subSymbolReplacements, 519 | }) 520 | } 521 | 522 | // Reference name is used for the first time, so we keep a reference to it, for the first condition above. 523 | else { 524 | this.refsDeclared.set(refName, [origRefSymbol]) 525 | 526 | this.debug('ref', 'to-declare:', refName) 527 | this.declarations.registerInternal(origRefSymbol, { 528 | default: false, 529 | export: false, 530 | // Declare the reference as the right name 531 | newName: origRefName !== refName ? refName : undefined, 532 | symbolReplacements: subSymbolReplacements, 533 | }) 534 | } 535 | } 536 | 537 | return symbolReplacements 538 | } 539 | 540 | private handleSymbolFromExternalLibrary(symbol: ts.Symbol, reExport = false): void { 541 | const { importKind, importName, moduleName } = this.findImportFromModule( 542 | symbol, 543 | this.program.isSourceFileFromExternalLibrary 544 | ) 545 | 546 | this.declarations.registerExternal(symbol, importKind, importName, moduleName, reExport) 547 | } 548 | 549 | private handleSymbolFromJsonFile(symbol: ts.Symbol, sourceFile: ts.SourceFile, reExport = false): void { 550 | const relativePath = ts.getRelativePathFromFile( 551 | this.entryFile.resolvedPath, 552 | sourceFile.resolvedPath, 553 | (fileName) => fileName 554 | ) 555 | const { importKind, importName } = this.findImportFromModule(symbol, ts.isJsonSourceFile) 556 | 557 | this.declarations.registerExternal(symbol, importKind, importName, relativePath, reExport) 558 | } 559 | 560 | private nameIsAlreadyExported(name: string | ts.__String): boolean { 561 | return this.symbols.exportNames.has(name as ts.__String) 562 | } 563 | 564 | private nameIsAlreadyGlobal(name: string | ts.__String): boolean { 565 | return this.symbols.globalNames.includes(name as ts.__String) 566 | } 567 | 568 | private nameIsAlreadyExportedOrGlobal(name: string | ts.__String): boolean { 569 | return this.nameIsAlreadyExported(name) || this.nameIsAlreadyGlobal(name) 570 | } 571 | 572 | private nameIsAlreadyUsebByRef(name: string | ts.__String): boolean { 573 | return this.refsDeclared.has(name as ts.__String) 574 | } 575 | 576 | private maybeTypeOf(name: ts.__String | string, ref: ts.Identifier | ts.ImportTypeNode): string { 577 | return ts.isImportTypeNode(ref) && ref.isTypeOf ? `typeof ${name}` : (name as string) 578 | } 579 | 580 | private shouldHandleExternalAugmentation(declaration: ts.Declaration): boolean { 581 | if (!this.declareAugmentationsOption) return false 582 | return !!findFirstParent(declaration, isExternalLibraryAugmentation) 583 | } 584 | 585 | private isExportedSymbol(symbol: ts.Symbol): boolean { 586 | return this.symbols.origSymbols.has(symbol) 587 | } 588 | 589 | private isImportedAsNamespaceSymbol(symbol: ts.Symbol): boolean { 590 | return symbol.declarations!.some(ts.isSourceFile) 591 | } 592 | 593 | private isGlobalSymbol(symbol: ts.Symbol): boolean { 594 | return this.symbols.globalSymbols.includes(symbol) 595 | } 596 | 597 | private isInternalGlobalSymbol(symbol: ts.Symbol): boolean { 598 | return this.symbols.internalGlobalSymbols.includes(symbol) 599 | } 600 | 601 | private debug(subject: 'export' | 'ref' | 'internal-global', ...messages: any[]) { 602 | if (this.debugSwitch) console.info(`[${subject.toUpperCase()}]`, ...messages) 603 | } 604 | 605 | // 606 | // ──────────────────────────────────────────────────────────────────────────── 607 | // :::::::: Finders :::::::: 608 | // ──────────────────────────────────────────────────────────────────────────── 609 | // 610 | 611 | private findOtherAliasName(origSymbol: ts.Symbol, origName: ts.__String): ts.__String { 612 | const aliased = Array.from(this.symbols.exportSymbols).find( 613 | ([otherExport, [otherOrig]]) => origSymbol === otherOrig && origName !== otherExport.escapedName 614 | ) 615 | 616 | if (!aliased) throw Error(`Aliased symbol of ${origName} was not found`) 617 | 618 | const [aliasSymbol, { 0: origAliasSymbol }] = aliased 619 | 620 | let aliasName: ts.__String | undefined = aliasSymbol.escapedName 621 | 622 | if (aliasName === ts.InternalSymbolName.Default) { 623 | aliasName = getDeclarationName(aliasSymbol.declarations![0]) 624 | } 625 | 626 | if (aliasName === ts.InternalSymbolName.Default) { 627 | aliasName = getDeclarationName(origAliasSymbol.declarations![0]) 628 | } 629 | 630 | if (!aliasName || aliasName === ts.InternalSymbolName.Default) { 631 | throw Error(`Unnamed alias (default export): ${origName}`) 632 | } 633 | 634 | return aliasName 635 | } 636 | 637 | private findOtherExportsWithSameOrig(exportSymbol: ts.Symbol, origSymbol: ts.Symbol): ts.Symbol[] | undefined { 638 | const others = Array.from(this.symbols.exportSymbols) 639 | .filter(([otherExport, [otherOrig]]) => otherOrig === origSymbol && otherExport !== exportSymbol) 640 | .map(([otherExport]) => otherExport) 641 | 642 | if (others.length) return others 643 | } 644 | 645 | /** 646 | * Finds the original import declaration from a module matching a predicate. 647 | */ 648 | private findImportFromModule( 649 | symbol: ts.Symbol, 650 | predicate: (sourceFile: ts.SourceFile) => boolean 651 | ): { 652 | importName: ts.__String 653 | importKind: ts.SyntaxKind 654 | moduleName: string 655 | } { 656 | const [firstDeclaration] = symbol.declarations! 657 | 658 | const moduleSpecifier = getModuleSpecifier(firstDeclaration) 659 | 660 | if (moduleSpecifier) { 661 | const moduleSymbol = this.checker.getSymbolAtLocation(moduleSpecifier) 662 | 663 | if (!moduleSymbol || !moduleSymbol.declarations) { 664 | throw Error('Could not resolve module symbol: ' + moduleSpecifier.getText()) // Should not happen 665 | } 666 | if (!(moduleSymbol.flags & ts.SymbolFlags.ValueModule)) { 667 | throw Error('Is not a proper module: ' + moduleSpecifier.getText()) // Should not happen 668 | } 669 | 670 | const moduleName = getModuleNameFromSpecifier(moduleSpecifier) 671 | 672 | if (!moduleName) { 673 | throw Error('Could not find module name: ' + moduleSpecifier.getText()) // Should not happen 674 | } 675 | 676 | // We need to check every declaration because of augmentations that could lead to false negatives. 677 | // Ex: the predicate `isSourceFileFromExternalLibrary` on a module augmentation declaration (internal). 678 | const found = moduleSymbol.declarations.some((d) => predicate(d.getSourceFile())) 679 | 680 | if (found) { 681 | let importName = symbol.escapedName 682 | const importKind = firstDeclaration.kind 683 | 684 | // Retrieve the imported property name in declarations like `import { A as B } from '...'` 685 | if (ts.isImportOrExportSpecifier(firstDeclaration) && firstDeclaration.propertyName) { 686 | importName = firstDeclaration.propertyName.escapedText 687 | } 688 | 689 | return { moduleName, importName, importKind } 690 | } 691 | } 692 | 693 | // If no module specifier is found (e.g. `export { A }`) 694 | // or if the resolved module does not match the predicate, 695 | // we go to the next aliased symbol. 696 | 697 | if (!(symbol.flags & ts.SymbolFlags.Alias)) { 698 | throw Error(`${symbol.escapedName} is not an aliased export symbol`) 699 | } 700 | 701 | const nextSymbol = this.checker.getImmediateAliasedSymbol(symbol) 702 | if (!nextSymbol || !nextSymbol.declarations) { 703 | throw Error(`Could not find an import matching the predicate for: ${symbol.escapedName}`) 704 | } 705 | 706 | return this.findImportFromModule(nextSymbol, predicate) 707 | } 708 | } 709 | -------------------------------------------------------------------------------- /src/bundle-addon/declaration-registrar.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import { strictEqual } from 'assert' 3 | import { Color } from '../utils/log' 4 | import { EitherOne } from '../utils/manipulation' 5 | import { getModifier, getNextKeyword, getDeclarationIdentifier } from './syntax-retrieval' 6 | import { isTopLevelVariableDeclaration } from './syntax-check' 7 | 8 | /** 9 | * @internal 10 | */ 11 | export type Replacement = { 12 | replace: ts.Node 13 | by: string 14 | } 15 | 16 | /** 17 | * Registrar dedicated to the actual storage of bundled declarations. 18 | * To use with the Declaration Collector. 19 | * @internal 20 | */ 21 | export class DeclarationRegistrar { 22 | imports: Set 23 | exports: Map 24 | globals: Map 25 | 26 | constructor() { 27 | this.imports = new Set() 28 | this.exports = new Map() 29 | this.globals = new Map() 30 | } 31 | 32 | registerInternal( 33 | origSymbol: ts.Symbol, 34 | options: { 35 | export: boolean 36 | default: boolean 37 | newName?: ts.__String | string 38 | symbolReplacements?: Replacement[][] 39 | } 40 | ) { 41 | if (!origSymbol.declarations) return 42 | 43 | const isNamespace = origSymbol.declarations.some(ts.isSourceFile) 44 | if (isNamespace) { 45 | throw Error(`File ${origSymbol.escapedName} is imported as a namespace, and thus can't be bundled.`) 46 | } 47 | 48 | const { export: exportKeyword, default: defaultKeyword, newName, symbolReplacements = [] } = options 49 | 50 | // tslint:disable-next-line: prefer-for-of 51 | for (let index = 0; index < origSymbol.declarations.length; index++) { 52 | const declaration = origSymbol.declarations[index] 53 | const declarationReplacements = symbolReplacements[index] 54 | 55 | const declarationText = this.getDeclarationText(declaration, { 56 | newName, 57 | replacements: declarationReplacements, 58 | exportKeyword, 59 | defaultKeyword, 60 | }) 61 | 62 | const jsdocText = this.getJSDocComment(declaration) 63 | 64 | this.exports.set(declarationText, jsdocText) 65 | } 66 | } 67 | 68 | registerAlias(name: ts.__String, alias?: EitherOne<{ as: ts.__String; default: true }>): void { 69 | if (alias && alias.as != null) { 70 | this.exports.set(`export { ${name} as ${alias.as} };`, '') 71 | } else if (alias && alias.default) { 72 | this.exports.set(`export default ${name};`, '') 73 | } else { 74 | this.exports.set(`export { ${name} };`, '') 75 | } 76 | } 77 | 78 | /** 79 | * Don't bundle original declarations from external libraries or from JSON files (with `resolveJsonModule`). 80 | */ 81 | registerExternal( 82 | symbol: ts.Symbol, 83 | importKind: ts.SyntaxKind, 84 | importName: ts.__String, 85 | modulePath: string, 86 | reExport = false 87 | ): void { 88 | const symbolName = symbol.escapedName 89 | 90 | if (symbol.declarations && symbol.declarations.length > 1) { 91 | console.warn(Color.yellow(`External/Json symbol with multiple/merged declarations not supported: ${symbolName}`)) 92 | } 93 | 94 | switch (importKind) { 95 | case ts.SyntaxKind.ExportSpecifier: 96 | case ts.SyntaxKind.ImportSpecifier: 97 | // Same name than imported name or default export. 98 | if (symbolName === importName || symbolName === ts.InternalSymbolName.Default) { 99 | this.imports.add(`import { ${importName} } from "${modulePath}";`) 100 | } 101 | // Different name than imported name. 102 | else { 103 | this.imports.add(`import { ${importName} as ${symbolName} } from "${modulePath}";`) 104 | } 105 | break 106 | 107 | case ts.SyntaxKind.ImportClause: // default import 108 | this.imports.add(`import ${importName} from "${modulePath}";`) 109 | break 110 | 111 | case ts.SyntaxKind.NamespaceImport: 112 | this.imports.add(`import * as ${importName} from "${modulePath}";`) 113 | break 114 | 115 | case ts.SyntaxKind.ImportEqualsDeclaration: 116 | this.imports.add(`import ${importName} = require("${modulePath}");`) 117 | break 118 | 119 | default: 120 | throw Error(`External/Json import not handled: ${importName} from "${modulePath}", kind: ${importKind}`) 121 | } 122 | 123 | if (reExport) { 124 | if (symbolName === ts.InternalSymbolName.Default) { 125 | this.exports.set(`export default ${importName};`, '') 126 | } else { 127 | this.exports.set(`export { ${symbolName} };`, '') 128 | } 129 | } 130 | } 131 | 132 | registerStar(modulePath: string) { 133 | this.exports.set(`export * from "${modulePath}";`, '') 134 | } 135 | 136 | registerGlobal(origSymbol: ts.Symbol) { 137 | for (const declaration of origSymbol.declarations || []) { 138 | const sourceFile = declaration.getSourceFile() 139 | 140 | let declarationText = declaration.getText(sourceFile) 141 | 142 | // Remove `declare` keyword of individual declarations. 143 | const declareModifier = getModifier(declaration, ts.SyntaxKind.DeclareKeyword) 144 | 145 | if (declareModifier) { 146 | const start = declareModifier.getStart(sourceFile) - declaration.getStart(sourceFile) 147 | const end = declareModifier.getEnd() - declaration.getStart(sourceFile) 148 | 149 | strictEqual( 150 | declarationText.slice(start, end), 151 | declareModifier.getText(sourceFile), 152 | `Declare modifier not found at start:${start} - end:${end}` 153 | ) 154 | 155 | declarationText = declarationText.slice(0, start) + declarationText.slice(end + 1) 156 | } 157 | 158 | const jsdocText = this.getJSDocComment(declaration) 159 | 160 | this.globals.set(declarationText, jsdocText) 161 | } 162 | } 163 | 164 | /** 165 | * Retrieves declaration text and applies changes on-the-fly. 166 | */ 167 | private getDeclarationText( 168 | declaration: ts.Declaration, 169 | options: { 170 | newName?: ts.__String | string 171 | replacements?: Replacement[] 172 | exportKeyword?: boolean 173 | defaultKeyword?: boolean 174 | } = {} 175 | ): string { 176 | const { newName, defaultKeyword, exportKeyword, replacements = [] } = options 177 | const sourceFile = declaration.getSourceFile() 178 | let text = declaration.getText(sourceFile) 179 | let prefix = '' 180 | 181 | if (newName) { 182 | const name = getDeclarationIdentifier(declaration as ts.Declaration) 183 | 184 | if (name) { 185 | replacements.push({ replace: name, by: newName as string }) 186 | } 187 | // Unnamed default declaration like `default function()`. 188 | else { 189 | const defaultModifier = getModifier(declaration, ts.SyntaxKind.DefaultKeyword) 190 | if (!defaultModifier) throw Error('Declaration without name: ' + text) 191 | 192 | // To insert the new name, we get the position of the keyword following `default` like `function` and add it. 193 | const nextKeyword = getNextKeyword(defaultModifier) 194 | if (!nextKeyword) throw Error("Can't find declaration keyword: " + text) 195 | 196 | const keywordAndNewName = nextKeyword.getText(sourceFile) + ' ' + newName 197 | 198 | // Move the end by one space to remove the empty space after the function name when inserting it. 199 | // @ts-expect-error readonly end 200 | if (nextKeyword.kind === ts.SyntaxKind.FunctionKeyword) nextKeyword.end++ 201 | 202 | replacements.push({ replace: nextKeyword, by: keywordAndNewName }) 203 | } 204 | } 205 | 206 | if (replacements.length) { 207 | // Reorder replacements to make changes starting from the end of the declaration, 208 | // to not affect their start position. 209 | replacements.sort(({ replace: a }, { replace: b }) => b.getStart(sourceFile) - a.getStart(sourceFile)) 210 | 211 | for (const { replace, by } of replacements) { 212 | const start = replace.getStart(sourceFile) - declaration.getStart(sourceFile) 213 | const end = replace.getEnd() - declaration.getStart(sourceFile) 214 | 215 | strictEqual( 216 | text.slice(start, end), 217 | replace.getText(sourceFile), 218 | `Entity name for replacement not found at start:${start} - end:${end}` 219 | ) 220 | 221 | text = text.slice(0, start) + by + text.slice(end) 222 | } 223 | } 224 | 225 | // We must handle modifiers after the replacements due to their position possibly being affected by keyword manipulation. 226 | if (isTopLevelVariableDeclaration(declaration)) { 227 | // Variable declarations are exported via their statement, two parents up. 228 | // There may be multiple variables declared in one statement. 229 | // We choose to split each of them into their own statement for easier manipulation. 230 | 231 | const exportModifier = getModifier(declaration, ts.SyntaxKind.ExportKeyword) 232 | const defaultModifier = getModifier(declaration, ts.SyntaxKind.DefaultKeyword) 233 | 234 | if ((exportModifier && exportKeyword !== false) || (!exportModifier && exportKeyword === true)) { 235 | prefix += 'export ' 236 | } 237 | 238 | // https://github.com/ajafff/tsutils/blob/v3.17.1/util/util.ts#L278-L284 239 | const keyword = 240 | declaration.parent.flags & ts.NodeFlags.Let 241 | ? 'let' 242 | : declaration.parent.flags & ts.NodeFlags.Const 243 | ? 'const' 244 | : 'var' 245 | 246 | if ((defaultModifier && defaultKeyword !== false) || (!defaultModifier && defaultKeyword === true)) { 247 | if (!exportModifier && exportKeyword !== true) { 248 | throw Error(`Default modifier should always be within an exported declaration: ${text}`) 249 | } 250 | prefix += `default ${keyword} ` 251 | } else { 252 | prefix += `declare ${keyword} ` 253 | } 254 | } else { 255 | const exportModifier = getModifier(declaration, ts.SyntaxKind.ExportKeyword) 256 | const defaultModifier = getModifier(declaration, ts.SyntaxKind.DefaultKeyword) 257 | const declareModifier = getModifier(declaration, ts.SyntaxKind.DeclareKeyword) 258 | 259 | // Add `export` keyword. 260 | if (!exportModifier && exportKeyword === true) { 261 | prefix += 'export ' 262 | } 263 | 264 | // Add `default` keyword, remove `declare`. Must have export keyword. 265 | if (!defaultModifier && defaultKeyword === true) { 266 | if (!exportModifier && exportKeyword !== true) { 267 | throw Error(`Can't add a default modifier on a non-exported declaration: ${text}`) 268 | } 269 | 270 | if (declareModifier) { 271 | const start = declareModifier.getStart(sourceFile) - declaration.getStart(sourceFile) 272 | const end = declareModifier.getEnd() - declaration.getStart(sourceFile) 273 | 274 | strictEqual( 275 | text.slice(start, end), 276 | declareModifier.getText(sourceFile), 277 | `Declare modifier not found at start:${start} - end:${end}` 278 | ) 279 | 280 | text = text.slice(0, start) + 'default' + text.slice(end) 281 | } 282 | // Interface declarations don't have a `declare` keyword. 283 | else { 284 | if (exportModifier) { 285 | const start = exportModifier.getStart(sourceFile) - declaration.getStart(sourceFile) 286 | const end = exportModifier.getEnd() - declaration.getStart(sourceFile) 287 | 288 | strictEqual( 289 | text.slice(start, end), 290 | exportModifier.getText(sourceFile), 291 | `Export modifier not found at start:${start} - end:${end}` 292 | ) 293 | 294 | text = text.slice(0, start) + 'export default' + text.slice(end) 295 | } 296 | // exportKeyword true: simply add export as a prefix. 297 | else { 298 | prefix += 'default ' 299 | } 300 | } 301 | } 302 | 303 | // Remove `default` keyword, add `declare`. 304 | if (defaultModifier && defaultKeyword === false) { 305 | const start = defaultModifier.getStart(sourceFile) - declaration.getStart(sourceFile) 306 | const end = defaultModifier.getEnd() - declaration.getStart(sourceFile) 307 | 308 | strictEqual( 309 | text.slice(start, end), 310 | defaultModifier.getText(sourceFile), 311 | `Default modifier not found at start:${start} - end:${end}` 312 | ) 313 | 314 | // Interface declarations don't have a `declare` keyword. 315 | text = ts.isInterfaceDeclaration(declaration) 316 | ? text.slice(0, start) + text.slice(end + 1) 317 | : text.slice(0, start) + 'declare' + text.slice(end) 318 | } 319 | 320 | // Remove `export` keyword. 321 | if (exportModifier && exportKeyword === false) { 322 | if (defaultModifier && defaultKeyword !== false) { 323 | throw Error(`Can't remove an export modifier on a default export: ${text}`) 324 | } 325 | 326 | const start = exportModifier.getStart(sourceFile) - declaration.getStart(sourceFile) 327 | const end = exportModifier.getEnd() - declaration.getStart(sourceFile) 328 | 329 | strictEqual( 330 | text.slice(start, end), 331 | exportModifier.getText(sourceFile), 332 | `Export modifier not found at start:${start} - end:${end}` 333 | ) 334 | 335 | text = text.slice(0, start) + text.slice(end + 1) 336 | } 337 | 338 | // Declarations from module augmentations lack a declare keyword (they don't have export and default keywords either). 339 | if ( 340 | !declareModifier && 341 | !exportModifier && 342 | !defaultModifier && 343 | defaultKeyword !== true && 344 | !ts.isInterfaceDeclaration(declaration) 345 | ) { 346 | prefix += 'declare ' 347 | } 348 | } 349 | 350 | return prefix + text 351 | } 352 | 353 | /** 354 | * Retrieves documentation comment from declaration. 355 | * @internal 356 | */ 357 | private getJSDocComment( 358 | declaration: ts.Declaration, 359 | sourceFile: ts.SourceFile = declaration.getSourceFile() 360 | ): string { 361 | let comment = '' 362 | 363 | const statement = isTopLevelVariableDeclaration(declaration) ? declaration.parent.parent : declaration 364 | 365 | // Get JSDoc comment block that is closest to the definition. 366 | const ranges = ts.getJSDocCommentRanges(statement, sourceFile.text) || [] 367 | const range = ranges[ranges.length - 1] 368 | 369 | if (range) { 370 | comment = sourceFile.text.substring(range.pos, range.end) 371 | // comment.substr(2, comment.length - 4) // remove /** */ 372 | } 373 | 374 | return comment 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /src/bundle-addon/directive-collector.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | 3 | /** 4 | * Retrieves possible directives from all internal files of the program. 5 | * @internal 6 | */ 7 | export function collectDirectives(program: ts.Program) { 8 | const typeRef = new Set() 9 | const libRef = new Set() 10 | 11 | for (const fileName of program.getRootFileNames()) { 12 | const sourceFile = program.getSourceFile(fileName)! 13 | 14 | for (const trd of sourceFile.typeReferenceDirectives) { 15 | // const name = sourceFile.text.substring(trd.pos, trd.end) 16 | typeRef.add(`/// `) 17 | } 18 | 19 | for (const lrd of sourceFile.libReferenceDirectives) { 20 | libRef.add(`/// `) 21 | } 22 | } 23 | 24 | return { typeRef, libRef } 25 | } 26 | -------------------------------------------------------------------------------- /src/bundle-addon/external-augmentation-collector.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import { isExternalLibraryAugmentation } from './syntax-check' 3 | 4 | /** 5 | * Retrieves external module augmentations. 6 | * `declare module 'lib' {}` 7 | * @internal 8 | */ 9 | export function collectExternalAugmentations(program: ts.Program) { 10 | const augmentations = new Set() 11 | 12 | nextSourceFile: for (const fileName of program.getRootFileNames()) { 13 | const sourceFile = program.getSourceFile(fileName)! 14 | 15 | // Ambient file 16 | if (!sourceFile.externalModuleIndicator) { 17 | for (const statement of sourceFile.statements) { 18 | // There can't be any `declare global` or `declare "./relative-module"` in ambient context. 19 | // - TS2669: Augmentations for the global scope can only be directly nested in external modules or ambient module declarations. 20 | // - TS2436: Ambient module declaration cannot specify relative module name. 21 | if (isExternalLibraryAugmentation(statement)) { 22 | augmentations.add(statement.getText()) 23 | } 24 | } 25 | 26 | continue nextSourceFile 27 | } 28 | 29 | // Module file 30 | for (const node of sourceFile.moduleAugmentations || []) { 31 | if (isExternalLibraryAugmentation(node.parent)) { 32 | augmentations.add(node.parent.getText()) 33 | } 34 | } 35 | } 36 | 37 | return augmentations 38 | } 39 | -------------------------------------------------------------------------------- /src/bundle-addon/index.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import { EmitOptions } from '../interfaces' 3 | import { Color } from '../utils/log' 4 | import { collectDirectives } from './directive-collector' 5 | import { collectExternalAugmentations } from './external-augmentation-collector' 6 | import { SymbolCollector } from './symbol-collector' 7 | import { DeclarationCollector } from './declaration-collector' 8 | import { print } from './printer' 9 | import { ensureAbsolutePath } from '../utils/path' 10 | 11 | /** 12 | * Intercepts the declaration file to a cache instead of writing it. 13 | * @internal 14 | */ 15 | export function getDtsInterceptor(dtsCache: Map): ts.WriteFileCallback { 16 | // https://github.com/microsoft/TypeScript/blob/v3.6.4/src/compiler/program.ts#L151-L171 17 | 18 | return (fileName, data, writeByteOrderMark, onError, sourceFiles) => { 19 | try { 20 | if (fileName.endsWith(ts.Extension.Dts) || fileName.endsWith('.d.ts.map')) { 21 | dtsCache.set(fileName, data) 22 | } else { 23 | ts.sys.writeFile(fileName, data, writeByteOrderMark) 24 | } 25 | } catch (err: any) { 26 | if (onError) onError(err.message) 27 | } 28 | } 29 | } 30 | 31 | /** 32 | * @internal 33 | */ 34 | export function bundleDts( 35 | program: ts.Program, 36 | dtsCache: Map, 37 | { 38 | entryPoint, 39 | fallbackOnError = true, 40 | globals: keepGlobals = true, 41 | augmentations: keepAugmentations = true, 42 | extras, 43 | }: EmitOptions.Bundle 44 | ) { 45 | const dtsOutDir = getDtsOutDir(program) 46 | const dtsProgram = createDtsProgram(program, dtsCache) 47 | const dtsOptions = dtsProgram.getCompilerOptions() 48 | 49 | let entryPoints = typeof entryPoint === 'string' ? [entryPoint] : entryPoint 50 | entryPoints = entryPoints.map((ep) => { 51 | ep = ep.replace(/(\.js|\.ts|\.d\.ts)$/m, '.d.ts') 52 | return ensureAbsolutePath(ep, dtsOutDir) 53 | }) 54 | 55 | try { 56 | // Retrieve all bundles before writing them, to fail on error before any IO. 57 | entryPoints 58 | .map((path) => { 59 | const entryFile = dtsProgram.getSourceFile(path) 60 | if (!entryFile) throw Error('Unable to load entry point:' + path) 61 | 62 | const directives = collectDirectives(dtsProgram) 63 | const augmentationsCollection = keepAugmentations ? collectExternalAugmentations(dtsProgram) : undefined 64 | const symbols = new SymbolCollector(entryFile, dtsProgram) 65 | const { declarations } = new DeclarationCollector( 66 | symbols, 67 | entryFile, 68 | dtsProgram, 69 | keepGlobals, 70 | keepAugmentations 71 | ) 72 | 73 | const globalsCollection = keepGlobals ? declarations.globals : undefined 74 | 75 | const bundled = print({ 76 | typeDirectivesCollection: directives.typeRef, 77 | libDirectivesCollection: directives.libRef, 78 | importsCollection: declarations.imports, 79 | exportsCollection: declarations.exports, 80 | globalsCollection, 81 | augmentationsCollection, 82 | extrasCollection: extras, 83 | newLine: dtsOptions.newLine, 84 | }) 85 | 86 | return [path, bundled] as const 87 | }) 88 | .forEach(([path, bundled]) => { 89 | ts.sys.writeFile(path, bundled) 90 | }) 91 | 92 | if (dtsOptions.listEmittedFiles) { 93 | console.log('Emitted files:\n' + entryPoints.join('\n')) 94 | } 95 | } catch (error: any) { 96 | if (!fallbackOnError) throw Color.red(error) 97 | 98 | console.error(Color.red(error.stack)) 99 | console.log('Fallback to original declaration files') 100 | 101 | dtsCache.forEach((data, path) => ts.sys.writeFile(path, data)) 102 | 103 | if (dtsOptions.listEmittedFiles) { 104 | console.log('Emitted files:\n' + Array.from(dtsCache.keys()).join('\n')) 105 | } 106 | } 107 | } 108 | 109 | /** 110 | * @internal 111 | */ 112 | function createDtsProgram(program: ts.Program, dtsCache: Map): ts.Program { 113 | const options = program.getCompilerOptions() 114 | 115 | // https://stackoverflow.com/a/53764522/4776628 116 | const host = ts.createCompilerHost(options, true) 117 | 118 | const readFile0 = host.readFile 119 | host.readFile = (fileName) => { 120 | if (dtsCache.has(fileName)) return dtsCache.get(fileName)! 121 | return readFile0.call(host, fileName) 122 | } 123 | 124 | const fileExists0 = host.fileExists 125 | host.fileExists = (fileName) => { 126 | if (dtsCache.has(fileName)) return true 127 | return fileExists0.call(host, fileName) 128 | } 129 | 130 | const getSourceFile0 = host.getSourceFile 131 | host.getSourceFile = (fileName, languageVersion, onError, shouldCreate) => { 132 | if (dtsCache.has(fileName)) { 133 | return ts.createSourceFile( 134 | fileName, 135 | dtsCache.get(fileName)!, 136 | options.target || /* default */ ts.ScriptTarget.ES5, 137 | true, 138 | ts.ScriptKind.TS 139 | ) 140 | } 141 | return getSourceFile0.call(host, fileName, languageVersion, onError, shouldCreate) 142 | } 143 | 144 | const rootNames = Array.from(dtsCache.keys()) 145 | 146 | return ts.createProgram({ rootNames, options, host }) 147 | } 148 | 149 | /** 150 | * Declaration output folder is either `declarationDir`, `outDir`, or the original root folder. 151 | * @internal 152 | */ 153 | function getDtsOutDir(program: ts.Program): string { 154 | const { declarationDir, outDir } = program.getCompilerOptions() 155 | return declarationDir || outDir || program.getCommonSourceDirectory() 156 | } 157 | -------------------------------------------------------------------------------- /src/bundle-addon/printer.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import { EmitOptions } from '../interfaces' 3 | 4 | /** 5 | * @internal 6 | */ 7 | export function print({ 8 | typeDirectivesCollection, 9 | libDirectivesCollection, 10 | importsCollection, 11 | exportsCollection, 12 | globalsCollection, 13 | augmentationsCollection, 14 | extrasCollection, 15 | newLine, 16 | }: { 17 | typeDirectivesCollection: Set 18 | libDirectivesCollection: Set 19 | importsCollection: Set 20 | exportsCollection: Map 21 | globalsCollection?: Map 22 | augmentationsCollection?: Set 23 | extrasCollection?: EmitOptions.Bundle.Extra[] 24 | newLine?: ts.NewLineKind 25 | }): string { 26 | const stringBuilder = new StringBuilder(newLine) 27 | 28 | extrasCollection = filterExtras(extrasCollection, [ 29 | typeDirectivesCollection, 30 | libDirectivesCollection, 31 | importsCollection, 32 | exportsCollection, 33 | globalsCollection, 34 | augmentationsCollection, 35 | ]) 36 | 37 | for (const directive of typeDirectivesCollection) { 38 | stringBuilder.addLine(directive) 39 | } 40 | if (typeDirectivesCollection.size) stringBuilder.addLine() 41 | 42 | for (const directive of libDirectivesCollection) { 43 | stringBuilder.addLine(directive) 44 | } 45 | if (libDirectivesCollection.size) stringBuilder.addLine() 46 | 47 | for (const declaration of importsCollection) { 48 | stringBuilder.addLine(declaration) 49 | } 50 | if (importsCollection.size) stringBuilder.addLine() 51 | 52 | for (const extra of extrasCollection) { 53 | if (extra.position === 'after-imports') { 54 | stringBuilder.addLine(extra.declaration).addLine() 55 | } 56 | } 57 | 58 | for (const [declaration, comment] of exportsCollection) { 59 | if (comment) stringBuilder.addLine(comment) 60 | stringBuilder.addLine(declaration).addLine() 61 | } 62 | 63 | for (const extra of extrasCollection) { 64 | if (extra.position === 'after-exports') { 65 | stringBuilder.addLine(extra.declaration).addLine() 66 | } 67 | } 68 | 69 | if (globalsCollection?.size) { 70 | stringBuilder.addLine('declare global {') 71 | 72 | for (const [declaration, comment] of globalsCollection) { 73 | if (comment) stringBuilder.addLine(comment) 74 | stringBuilder.addLine(declaration) 75 | } 76 | 77 | stringBuilder.addLine('}').addLine() 78 | } 79 | 80 | if (augmentationsCollection?.size) { 81 | for (const declaration of augmentationsCollection) { 82 | stringBuilder.addLine(declaration).addLine() 83 | } 84 | } 85 | 86 | stringBuilder.addLine('export {}') 87 | 88 | return stringBuilder.toString() 89 | } 90 | 91 | function filterExtras( 92 | extras: EmitOptions.Bundle.Extra[] | undefined, 93 | collections: (Set | Map | undefined)[] 94 | ): EmitOptions.Bundle.Extra[] { 95 | if (!extras) return [] 96 | 97 | const declarationsCollections = collections.map((col) => { 98 | return col instanceof Set ? Array.from(col) : col instanceof Map ? Array.from(col.values()) : [] 99 | }) 100 | 101 | return extras.filter((extra) => { 102 | return declarationsCollections.every((declarations) => !declarations.includes(extra.declaration)) 103 | }) 104 | } 105 | 106 | /** 107 | * Allows a large text string to be constructed incrementally by appending small chunks. 108 | * The final string can be obtained by calling StringBuilder.toString(). 109 | * 110 | * A naive approach might use the `+=` operator to append strings: 111 | * This would have the downside of copying the entire string each time a chunk is appended. 112 | * 113 | * @internal 114 | */ 115 | class StringBuilder { 116 | private chunks: string[] = [] 117 | private newLine: '\n' | '\r\n' 118 | 119 | constructor(newLineKind?: ts.NewLineKind) { 120 | this.newLine = 121 | newLineKind === ts.NewLineKind.CarriageReturnLineFeed 122 | ? '\r\n' 123 | : newLineKind === ts.NewLineKind.LineFeed 124 | ? '\n' 125 | : (ts.sys.newLine as '\r\n' | '\n') 126 | } 127 | 128 | add(text: string) { 129 | this.chunks.push(text) 130 | return this 131 | } 132 | 133 | addLine(text: string = '') { 134 | if (text.length > 0) this.add(text) 135 | return this.add(this.newLine) 136 | } 137 | 138 | toString(): string { 139 | if (this.chunks.length === 0) return '' 140 | 141 | if (this.chunks.length > 1) { 142 | const joined = this.chunks.join('') 143 | this.chunks.length = 1 144 | this.chunks[0] = joined 145 | } 146 | 147 | return this.chunks[0] 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/bundle-addon/symbol-collector.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import { Color } from '../utils/log' 3 | import { mapToSet } from '../utils/manipulation' 4 | import { findFirstChild, getModuleName } from './syntax-retrieval' 5 | 6 | /** 7 | * @internal 8 | */ 9 | export type Reference = { 10 | // Symbols with multiple declarations can have different references per declaration, 11 | // so we must keep track of the declaration. 12 | declarationIndex: number 13 | ref: ts.Identifier | ts.ImportTypeNode 14 | subrefs: Reference[] 15 | } 16 | 17 | /** 18 | * @internal 19 | */ 20 | export class SymbolCollector { 21 | /** 22 | * Star exported name from external libraries. 23 | * @example export * from '@org/package' 24 | */ 25 | externalStarModuleNames: Set 26 | 27 | /** 28 | * Exported/Exposed symbols at the root, 29 | * with their associated aliased original symbol (may be in different file than the root), 30 | * and all the other symbols referenced and needed by them. 31 | * 32 | * @example 33 | * export { myFunction } from './my-function' // aliased 34 | * export const myVariable = {} // not aliased 35 | */ 36 | exportSymbols: Map 37 | 38 | /** 39 | * Index of exported symbol names at the root. 40 | */ 41 | exportNames: Set 42 | 43 | /** 44 | * Index of original symbols associated with the exported symbols. 45 | */ 46 | origSymbols: Set 47 | 48 | /** 49 | * All globals declared (external and internal). 50 | * @example Array, Partial, etc. 51 | */ 52 | globalSymbols: ts.Symbol[] 53 | 54 | /** 55 | * Index of declared global symbol names. 56 | */ 57 | globalNames: ts.__String[] 58 | 59 | /** 60 | * Globals declared internally, either in a `declare global` statement inside a module file, 61 | * or directly with a `declare` statement inside an ambient file (file without import/export). 62 | * @example declare const myGlobalVariable = {} 63 | */ 64 | internalGlobalSymbols: ts.Symbol[] 65 | 66 | private program: ts.Program 67 | private checker: ts.TypeChecker 68 | private entryFile: ts.SourceFile 69 | private entrySymbol: ts.Symbol 70 | 71 | constructor(entryFile: ts.SourceFile, program: ts.Program) { 72 | this.program = program 73 | this.checker = program.getTypeChecker() 74 | this.entryFile = entryFile 75 | 76 | const entrySymbol = this.checker.getSymbolAtLocation(this.entryFile) 77 | if (!entrySymbol) throw Error('Entry point is not a module (no export/import declaration).') 78 | this.entrySymbol = entrySymbol 79 | 80 | const { externalStarExports, externalStarModuleNames } = this.getExternalStarExports() 81 | const entryExports = this.getModuleExports(externalStarExports) 82 | 83 | this.externalStarModuleNames = externalStarModuleNames 84 | this.exportSymbols = this.getExportSymbolsMap(entryExports) 85 | this.globalSymbols = this.getGlobalSymbols() 86 | this.internalGlobalSymbols = this.getInternalGlobalSymbols(this.globalSymbols) 87 | 88 | // Build indexes for easier checks. 89 | this.origSymbols = mapToSet(this.exportSymbols, { 90 | mapper: ([origSymbol]) => origSymbol, 91 | }) 92 | this.exportNames = mapToSet(this.exportSymbols, { 93 | mapper: (_, s) => s.escapedName, 94 | filter: (_, s) => s.escapedName !== ts.InternalSymbolName.Default, // Don't index default name. 95 | }) 96 | 97 | this.globalNames = this.globalSymbols.map((s) => s.escapedName) 98 | } 99 | 100 | /** 101 | * Retrieves exported symbols. 102 | * {@linkcode SymbolCollector.exportSymbols } 103 | */ 104 | private getExportSymbolsMap(moduleExports: ts.Symbol[]) { 105 | // Map export symbol to its original one (might be the same), both ways, for indexing and easier retrieving. 106 | const exportSymbols = new Map() 107 | 108 | for (const exportSymbol of moduleExports) { 109 | let origSymbol = exportSymbol 110 | 111 | // Retrieve the original/aliased symbol. 112 | if (exportSymbol.flags & ts.SymbolFlags.Alias) { 113 | origSymbol = this.checker.getAliasedSymbol(exportSymbol) 114 | } 115 | 116 | // Ignore symbols without declarations. 117 | if (!origSymbol.declarations || !origSymbol.declarations.length) { 118 | console.warn(Color.yellow(`Symbol ${origSymbol.name} does not have any declaration`)) 119 | continue 120 | } 121 | 122 | const refs = this.getReferences(origSymbol) 123 | 124 | exportSymbols.set(exportSymbol, [origSymbol, refs]) 125 | } 126 | 127 | return exportSymbols 128 | } 129 | 130 | /** 131 | * Retrieves unexported symbols used by exported symbols. 132 | */ 133 | private getReferences(origSymbol: ts.Symbol, symbolsChain: ts.Symbol[] = []): Reference[] { 134 | if (!origSymbol.declarations) return [] 135 | 136 | // Don't search in external symbol declarations. 137 | // We need to check every declaration because of augmentations that could lead to false negatives. 138 | if ( 139 | origSymbol.declarations.some((d) => this.program.isSourceFileFromExternalLibrary(d.getSourceFile())) || 140 | origSymbol.declarations.some((d) => this.program.isSourceFileDefaultLibrary(d.getSourceFile())) 141 | ) { 142 | return [] 143 | } 144 | 145 | // Keep parent symbols in an array to avoid infinite loop when looking for circular subreferences. 146 | symbolsChain.push(origSymbol) 147 | 148 | const refs: Reference[] = [] 149 | 150 | // tslint:disable-next-line: prefer-for-of 151 | for (let declarationIndex = 0; declarationIndex < origSymbol.declarations.length; declarationIndex++) { 152 | const declaration = origSymbol.declarations[declarationIndex] 153 | 154 | if (ts.isSourceFile(declaration)) continue 155 | 156 | const getSubReferences = (identifier: ts.Identifier): Reference[] => { 157 | let refSymbol = this.checker.getSymbolAtLocation(identifier) 158 | 159 | // Avoid infinite loop due to circular references. 160 | if (!refSymbol || symbolsChain.includes(refSymbol)) return [] 161 | 162 | if (refSymbol.flags & ts.SymbolFlags.Alias) { 163 | refSymbol = this.checker.getAliasedSymbol(refSymbol) 164 | } 165 | 166 | if (!refSymbol.declarations || !refSymbol.declarations.length) return [] 167 | 168 | return this.getReferences(refSymbol, symbolsChain) 169 | } 170 | 171 | declaration.forEachChild(function visit(child) { 172 | // Taken from https://github.com/microsoft/rushstack/blob/2cb32ec198/apps/api-extractor/src/analyzer/AstSymbolTable.ts#L298 173 | if ( 174 | ts.isTypeReferenceNode(child) || 175 | ts.isExpressionWithTypeArguments(child) || // "extends" 176 | ts.isComputedPropertyName(child) || // [Prop]: 177 | ts.isTypeQueryNode(child) // "typeof X" 178 | ) { 179 | const identifier = findFirstChild(child, ts.isIdentifier) 180 | 181 | if (identifier) { 182 | const subrefs = getSubReferences(identifier) 183 | 184 | refs.push({ ref: identifier, subrefs, declarationIndex }) 185 | } 186 | } 187 | 188 | if (ts.isImportTypeNode(child)) { 189 | refs.push({ ref: child, subrefs: [], declarationIndex }) 190 | } 191 | 192 | child.forEachChild(visit) 193 | }) 194 | } 195 | 196 | return refs 197 | } 198 | 199 | /** 200 | * Retrieves exports from file in right order. 201 | */ 202 | private getModuleExports(excludes: Set = new Set()): ts.Symbol[] { 203 | if (this.entrySymbol.escapedName === 'globalThis') { 204 | return this.checker.getSymbolsInScope(this.entryFile, -1 as any) 205 | } 206 | 207 | const symbols = this.checker.getExportsOfModule(this.entrySymbol) 208 | const entryFilePositions = this.getExportsPositions(this.entryFile) 209 | 210 | // console.log(symbols.every((s) => entryFilePositions.has(s))) 211 | // console.log(Array.from(entryFilePositions).map(([s, n]) => s.name + ':' + n)) 212 | 213 | const sorted = symbols 214 | // Don't collect symbols from star exported external libraries. 215 | .filter((symbol) => !excludes.has(symbol)) 216 | .sort((symbolA, symbolB) => { 217 | const posA = entryFilePositions.get(symbolA) 218 | if (posA == null) { 219 | console.warn(Color.yellow(`export position not found for ${symbolA.escapedName}`)) 220 | return 0 221 | } 222 | 223 | const posB = entryFilePositions.get(symbolB) 224 | if (posB == null) { 225 | console.warn(Color.yellow(`export position not found for ${symbolA.escapedName}`)) 226 | return 0 227 | } 228 | 229 | return posA - posB 230 | }) 231 | 232 | return sorted 233 | } 234 | 235 | /** 236 | * Retrieves export position in entry file so we have original order of exports, 237 | * because `getExportsOfModule` places star exports last. 238 | */ 239 | private getExportsPositions(entryFile: ts.SourceFile): Map { 240 | const entryFilePositions = new Map() 241 | 242 | const getAliasedExportsPositions = (moduleSymbol: ts.Symbol) => { 243 | if (!moduleSymbol.exports) return 244 | 245 | moduleSymbol.exports.forEach((symbol, name) => { 246 | if (name === ts.InternalSymbolName.ExportStar) return 247 | const pos = symbol.declarations![0].getStart(entryFile) 248 | entryFilePositions.set(symbol, pos) 249 | }) 250 | } 251 | 252 | const getInternalStarExportsPositions = (moduleSymbol: ts.Symbol, entryExportDeclaration?: ts.Declaration) => { 253 | if (!moduleSymbol.exports) return 254 | 255 | const starExportSymbol = moduleSymbol.exports.get(ts.InternalSymbolName.ExportStar) 256 | if (!starExportSymbol) return 257 | 258 | for (const exportDeclaration of starExportSymbol.declarations || []) { 259 | if (!ts.isExportDeclaration(exportDeclaration)) continue 260 | 261 | const resolved = this.getResolvedModule(exportDeclaration) 262 | 263 | if (resolved.isExternalLibraryImport === true) continue 264 | 265 | for (const symbol of this.checker.getExportsOfModule(resolved.moduleSymbol)) { 266 | // Keep reference of the entry file declaration for nested star exports. 267 | if (entryExportDeclaration) { 268 | const pos = entryExportDeclaration.getStart(entryFile) 269 | entryFilePositions.set(symbol, pos) 270 | } else { 271 | const pos = exportDeclaration.getStart(entryFile) 272 | entryFilePositions.set(symbol, pos) 273 | } 274 | } 275 | 276 | getInternalStarExportsPositions(resolved.moduleSymbol, exportDeclaration) 277 | } 278 | } 279 | 280 | getAliasedExportsPositions(this.entrySymbol) 281 | getInternalStarExportsPositions(this.entrySymbol) 282 | 283 | return entryFilePositions 284 | } 285 | 286 | /** 287 | * Not always reliable, since we also get global symbols from dev packages like jest (`it`). 288 | * @see https://github.com/microsoft/rushstack/issues/1316 289 | * @see https://github.com/rbuckton/typedoc-plugin-biblio/blob/master/src/plugin.ts 290 | */ 291 | private getGlobalSymbols(): ts.Symbol[] { 292 | const allSourceFiles = this.program.getSourceFiles() 293 | const globalSourceFile = 294 | allSourceFiles.find((sf) => sf.hasNoDefaultLib) || allSourceFiles.find((sf) => !ts.isExternalModule(sf)) 295 | 296 | return this.checker.getSymbolsInScope(globalSourceFile!, -1 as any) // ts.SymbolFlags.All 297 | } 298 | 299 | /** 300 | * {@linkcode SymbolCollector.internalGlobalSymbols} 301 | */ 302 | private getInternalGlobalSymbols(globalSymbols: ts.Symbol[]): ts.Symbol[] { 303 | return globalSymbols.filter((symbol) => { 304 | if (!symbol.declarations?.length) return false 305 | 306 | const [firstDeclaration] = symbol.declarations 307 | 308 | // External module augmentations (`declare module 'lib'`) are detected as globals in ambient files. 309 | if (ts.isAmbientModule(firstDeclaration)) return false 310 | 311 | const sourceFile = firstDeclaration.getSourceFile() 312 | 313 | return ( 314 | !this.program.isSourceFileFromExternalLibrary(sourceFile) && 315 | !this.program.isSourceFileDefaultLibrary(sourceFile) 316 | ) 317 | }) 318 | } 319 | 320 | /** 321 | * Recursively retrieves symbols star exported from external libraries 322 | * so we don't manipulate them later and simply expose them as `export * from "lib"`. 323 | * {@linkcode SymbolCollector.externalStarModuleNames} 324 | */ 325 | private getExternalStarExports() { 326 | const externalStarExports = new Set() 327 | const externalStarModuleNames = new Set() 328 | 329 | const recurse = (moduleSymbol: ts.Symbol) => { 330 | if (!moduleSymbol.exports) return 331 | 332 | const starExportSymbol = moduleSymbol.exports.get(ts.InternalSymbolName.ExportStar) 333 | if (!starExportSymbol) return 334 | 335 | for (const declaration of starExportSymbol.declarations || []) { 336 | if (!ts.isExportDeclaration(declaration)) continue 337 | 338 | const resolved = this.getResolvedModule(declaration) 339 | 340 | if (resolved.isExternalLibraryImport === true) { 341 | for (const symbol of this.checker.getExportsOfModule(resolved.moduleSymbol)) { 342 | externalStarExports.add(symbol) 343 | externalStarModuleNames.add(resolved.moduleName) 344 | } 345 | } else { 346 | recurse(resolved.moduleSymbol) 347 | } 348 | } 349 | } 350 | 351 | recurse(this.entrySymbol) 352 | 353 | return { externalStarExports, externalStarModuleNames } 354 | } 355 | 356 | /** 357 | * Resolves modules, made for star exported declarations (all have an module specifier). 358 | * `export * from "module"` 359 | */ 360 | private getResolvedModule(declaration: ts.ExportDeclaration): ResolvedModule { 361 | const moduleName = getModuleName(declaration) 362 | if (!moduleName) throw Error('Could not get module specifier from: ' + declaration.getText()) 363 | 364 | const resolvedModule = ts.getResolvedModule(declaration.getSourceFile(), moduleName) 365 | 366 | if (resolvedModule) { 367 | const sourceFile = this.program.getSourceFile(resolvedModule.resolvedFileName) 368 | if (!sourceFile) throw Error('Could not locate file: ' + resolvedModule.resolvedFileName) 369 | const moduleSymbol = this.checker.getSymbolAtLocation(sourceFile)! 370 | 371 | return { 372 | moduleName, 373 | fileName: resolvedModule.resolvedFileName, 374 | sourceFile, 375 | moduleSymbol, 376 | isExternalLibraryImport: resolvedModule.isExternalLibraryImport || false, 377 | } 378 | } 379 | 380 | // The 'module' keyword can be used to declare multiple modules in a library (e.g. with '@types/node'), 381 | // so we can't resolve them properly with 'ts.getResolvedModule'. 382 | else { 383 | // A relative name should have been picked by 'ts.getResolvedModule'. 384 | if (ts.isExternalModuleNameRelative(moduleName)) { 385 | throw Error('Could not resolve module: ' + moduleName) 386 | } 387 | 388 | const moduleSymbol = this.checker.getSymbolAtLocation(declaration.moduleSpecifier! /* checked by getModuleName */) 389 | 390 | if (!moduleSymbol || !(moduleSymbol.flags & ts.SymbolFlags.ValueModule) || !moduleSymbol.declarations) { 391 | throw Error('Could not resolve symbol of module: ' + moduleName) 392 | } 393 | 394 | const sourceFile = moduleSymbol.declarations[0].getSourceFile() 395 | 396 | return { 397 | moduleName, 398 | fileName: sourceFile.fileName, 399 | sourceFile, 400 | moduleSymbol, 401 | isExternalLibraryImport: this.program.isSourceFileFromExternalLibrary(sourceFile), 402 | } 403 | } 404 | } 405 | } 406 | 407 | /** 408 | * @internal 409 | */ 410 | type ResolvedModule = { 411 | moduleName: string 412 | fileName: string 413 | sourceFile: ts.SourceFile 414 | moduleSymbol: ts.Symbol 415 | isExternalLibraryImport: boolean 416 | } 417 | -------------------------------------------------------------------------------- /src/bundle-addon/syntax-check.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | 3 | /** 4 | * @see https://github.com/microsoft/TypeScript/blob/v3.5.3/src/services/refactors/moveToNewFile.ts#L644 5 | * @internal 6 | */ 7 | export function isTopLevelDeclaration(node: ts.Node): node is TopLevelDeclaration { 8 | return ( 9 | (isTopLevelNonVariableDeclaration(node) && ts.isSourceFile(node.parent)) || 10 | (isTopLevelVariableDeclaration(node) && ts.isSourceFile(node.parent.parent.parent)) 11 | ) 12 | } 13 | 14 | /** 15 | * @internal 16 | */ 17 | export function isExportedTopLevelDeclaration(node: ts.Node): node is TopLevelDeclaration { 18 | return ( 19 | (isTopLevelNonVariableDeclaration(node) && isExported(node)) || 20 | (isTopLevelVariableDeclaration(node) && isExported(node.parent.parent)) 21 | ) 22 | } 23 | 24 | /** 25 | * @internal 26 | */ 27 | export function isTopLevelVariableDeclaration(node: ts.Node): node is TopLevelVariableDeclaration { 28 | return ( 29 | ts.isVariableDeclaration(node) && 30 | ts.isVariableDeclarationList(node.parent) && 31 | ts.isVariableStatement(node.parent.parent) 32 | ) 33 | } 34 | 35 | /** 36 | * @internal 37 | */ 38 | export function isTopLevelDeclarationStatement(node: ts.Node): node is TopLevelDeclarationStatement { 39 | return isTopLevelNonVariableDeclaration(node) || ts.isVariableStatement(node) 40 | } 41 | 42 | /** 43 | * @see https://github.com/microsoft/TypeScript/blob/v3.5.3/src/services/refactors/moveToNewFile.ts#L733 44 | * @internal 45 | */ 46 | export function isExported(dec: TopLevelDeclarationStatement): boolean { 47 | return ts.hasModifier(dec, ts.ModifierFlags.Export) 48 | } 49 | 50 | /** 51 | * @internal 52 | */ 53 | export function isTopLevelNonVariableDeclaration(node: ts.Node): node is TopLevelNonVariableDeclaration { 54 | switch (node.kind) { 55 | case ts.SyntaxKind.FunctionDeclaration: 56 | case ts.SyntaxKind.ClassDeclaration: 57 | case ts.SyntaxKind.ModuleDeclaration: 58 | case ts.SyntaxKind.EnumDeclaration: 59 | case ts.SyntaxKind.TypeAliasDeclaration: 60 | case ts.SyntaxKind.InterfaceDeclaration: 61 | case ts.SyntaxKind.ImportEqualsDeclaration: 62 | return true 63 | default: 64 | return false 65 | } 66 | } 67 | 68 | /** 69 | * Accepts ambient declarations like `declare "library"` and not `declare global` or `declare "./relative-module"`. 70 | * @internal 71 | */ 72 | export function isExternalLibraryAugmentation(node: ts.Node): node is ts.AmbientModuleDeclaration { 73 | return ( 74 | ts.isAmbientModule(node) && !ts.isGlobalScopeAugmentation(node) && !ts.isExternalModuleNameRelative(node.name.text) 75 | ) 76 | } 77 | 78 | export function isTopLevelNamedDeclaration(node: ts.Node): node is TopLevelNamedDeclaration { 79 | switch (node.kind) { 80 | case ts.SyntaxKind.FunctionDeclaration: 81 | case ts.SyntaxKind.ClassDeclaration: 82 | case ts.SyntaxKind.EnumDeclaration: 83 | case ts.SyntaxKind.TypeAliasDeclaration: 84 | case ts.SyntaxKind.InterfaceDeclaration: 85 | case ts.SyntaxKind.ModuleDeclaration: 86 | case ts.SyntaxKind.VariableDeclaration: 87 | return true 88 | default: 89 | return false 90 | } 91 | } 92 | 93 | /** 94 | * @internal 95 | */ 96 | export function isKeywordTypeNode(node: ts.Node): node is ts.KeywordTypeNode { 97 | switch (node.kind) { 98 | case ts.SyntaxKind.AnyKeyword: 99 | case ts.SyntaxKind.UnknownKeyword: 100 | case ts.SyntaxKind.NumberKeyword: 101 | case ts.SyntaxKind.BigIntKeyword: 102 | case ts.SyntaxKind.ObjectKeyword: 103 | case ts.SyntaxKind.BooleanKeyword: 104 | case ts.SyntaxKind.StringKeyword: 105 | case ts.SyntaxKind.SymbolKeyword: 106 | case ts.SyntaxKind.ThisKeyword: 107 | case ts.SyntaxKind.VoidKeyword: 108 | case ts.SyntaxKind.UndefinedKeyword: 109 | case ts.SyntaxKind.NullKeyword: 110 | case ts.SyntaxKind.NeverKeyword: 111 | return true 112 | default: 113 | return false 114 | } 115 | } 116 | 117 | /** 118 | * @internal 119 | */ 120 | export function hasModuleSpecifier( 121 | node: ts.ImportOrExportSpecifier | ts.ImportDeclaration | ts.ExportDeclaration 122 | ): boolean { 123 | if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) { 124 | return !!node.moduleSpecifier 125 | } 126 | 127 | if (ts.isImportSpecifier(node)) { 128 | return !!node.parent.parent.parent.moduleSpecifier 129 | } 130 | 131 | if (ts.isExportSpecifier(node)) { 132 | return !!node.parent.parent.moduleSpecifier 133 | } 134 | 135 | return false 136 | } 137 | 138 | /** 139 | * Ensures that the node has no children. 140 | * @see https://github.com/ajafff/tsutils/blob/v3.17.1/util/util.ts#L17-L19 141 | * @internal 142 | */ 143 | export function isTokenNode(node: ts.Node): boolean { 144 | return node.kind >= ts.SyntaxKind.FirstToken && node.kind <= ts.SyntaxKind.LastToken 145 | } 146 | 147 | /** 148 | * @internal 149 | * @see https://github.com/ajafff/tsutils/blob/v3.17.1/util/util.ts#L37-L39 150 | */ 151 | export function isKeywordNode(node: ts.Node): boolean { 152 | return node.kind >= ts.SyntaxKind.FirstKeyword && node.kind <= ts.SyntaxKind.LastKeyword 153 | } 154 | 155 | /** 156 | * @internal 157 | */ 158 | export type TopLevelDeclarationStatement = TopLevelNonVariableDeclaration | ts.VariableStatement 159 | 160 | /** 161 | * @internal 162 | */ 163 | export type TopLevelDeclaration = TopLevelNonVariableDeclaration | TopLevelVariableDeclaration | ts.BindingElement 164 | 165 | /** 166 | * @internal 167 | */ 168 | export type TopLevelVariableDeclaration = ts.VariableDeclaration & { 169 | parent: ts.VariableDeclarationList & { parent: ts.VariableStatement } 170 | } 171 | 172 | /** 173 | * @internal 174 | */ 175 | export type TopLevelNamedDeclaration = 176 | | ts.FunctionDeclaration 177 | | ts.ClassDeclaration 178 | | ts.EnumDeclaration 179 | | ts.TypeAliasDeclaration 180 | | ts.InterfaceDeclaration 181 | | ts.ModuleDeclaration 182 | | ts.VariableDeclaration 183 | 184 | /** 185 | * @internal 186 | */ 187 | export type TopLevelNonVariableDeclaration = 188 | | ts.FunctionDeclaration 189 | | ts.ClassDeclaration 190 | | ts.EnumDeclaration 191 | | ts.TypeAliasDeclaration 192 | | ts.InterfaceDeclaration 193 | | ts.ModuleDeclaration 194 | | ts.ImportEqualsDeclaration 195 | | TopLevelExpressionStatement 196 | 197 | /** 198 | * `exports.x = ...` 199 | * @internal 200 | */ 201 | export type TopLevelExpressionStatement = ts.ExpressionStatement & { 202 | expression: ts.BinaryExpression & { left: ts.PropertyAccessExpression } 203 | } 204 | -------------------------------------------------------------------------------- /src/bundle-addon/syntax-retrieval.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import { isTokenNode, isKeywordNode } from './syntax-check' 3 | 4 | /** 5 | * From `A` in `type T = A.a`, retrieves the root `A.a` and the property `a`. 6 | * @returns root/parent reference, and the property found. 7 | * @internal 8 | */ 9 | export function lookForProperty(ref: ts.Identifier | ts.ImportTypeNode) { 10 | let refRoot: ts.QualifiedName | ts.ImportTypeNode | undefined 11 | let refProp: ts.Identifier | undefined 12 | 13 | if (ts.isImportTypeNode(ref)) { 14 | refRoot = ref 15 | if (!ref.qualifier) return 16 | 17 | refProp = ts.isQualifiedName(ref.qualifier) 18 | ? ref.qualifier.right 19 | : ts.isIdentifier(ref.qualifier) 20 | ? ref.qualifier 21 | : undefined 22 | 23 | if (!refProp) return 24 | } else { 25 | refRoot = findFirstParent(ref, ts.isQualifiedName) 26 | if (!refRoot) return 27 | 28 | refProp = refRoot.right 29 | } 30 | 31 | return [refRoot, refProp] as [ts.QualifiedName | ts.ImportTypeNode, ts.Identifier] 32 | } 33 | 34 | /** 35 | * Does a depth-first search of the children of the specified node. 36 | * @internal 37 | */ 38 | export function findFirstChild( 39 | node: ts.Node, 40 | predicate: (childNode: ts.Node) => childNode is T, 41 | sourceFile = node.getSourceFile() 42 | ): T | undefined { 43 | for (const child of node.getChildren(sourceFile)) { 44 | if (predicate(child)) return child as T 45 | 46 | const grandChild = findFirstChild(child, predicate, sourceFile) 47 | if (grandChild) return grandChild as T 48 | } 49 | 50 | return undefined 51 | } 52 | 53 | /** 54 | * @internal 55 | */ 56 | export function findFirstParent( 57 | node: ts.Node, 58 | predicate: (parentNode: ts.Node) => parentNode is T 59 | ): T | undefined { 60 | let current: ts.Node | undefined = node.parent 61 | 62 | while (current) { 63 | if (predicate(current)) return current as T 64 | current = current.parent 65 | } 66 | 67 | return undefined 68 | } 69 | 70 | /** 71 | * @internal 72 | */ 73 | export function getNextToken(node: ts.Node, sourceFile = node.getSourceFile()): ts.Node | undefined { 74 | const children = node.parent.getChildren(sourceFile) 75 | 76 | for (const child of children) { 77 | if (child.end > node.end && child.kind !== ts.SyntaxKind.JSDocComment) { 78 | if (isTokenNode(child)) return child 79 | 80 | // Next token is nested in another node 81 | return getNextToken(child, sourceFile) 82 | } 83 | } 84 | } 85 | 86 | /** 87 | * @internal 88 | */ 89 | export function getNextKeyword(node: ts.Node, sourceFile = node.getSourceFile()): ts.Node | undefined { 90 | const children = node.parent.getChildren(sourceFile) 91 | 92 | for (const child of children) { 93 | if (child.end > node.end) { 94 | if (isKeywordNode(child)) return child 95 | continue 96 | } 97 | } 98 | } 99 | 100 | /** 101 | * @see https://github.com/Microsoft/TypeScript/blob/v3.6.4/src/compiler/utilities.ts#L5216-L5219 102 | * @internal 103 | */ 104 | export function getDeclarationIdentifier(node: ts.Declaration | ts.Expression): ts.Identifier | undefined { 105 | const name = ts.getNameOfDeclaration(node) 106 | return name && ts.isIdentifier(name) ? name : undefined 107 | } 108 | 109 | /** 110 | * Try to get A in `export default A`. 111 | * @internal 112 | */ 113 | export function getDeclarationName(node: ts.Declaration): ts.__String | undefined { 114 | const name = ts.getNameOfDeclaration(node) 115 | return name && ts.isIdentifier(name) ? name.escapedText : undefined 116 | } 117 | 118 | /** 119 | * @internal 120 | */ 121 | export function getModifier(node: ts.Node, kind: ts.Modifier['kind']): ts.Modifier | undefined { 122 | if (!node.modifiers) return 123 | 124 | for (const modifier of node.modifiers) { 125 | if (modifier.kind === kind) return modifier 126 | } 127 | } 128 | 129 | /** 130 | * Get module name from any import/export declaration part. Could be internal or external. 131 | * Example: X in `export * from "X"`. 132 | * 133 | * @param strict - throw an error if node is not part of an import/export declaration. 134 | * @internal 135 | */ 136 | export function getModuleName(node: ts.Node, strict = false): string | undefined { 137 | if (ts.isNamespaceExportDeclaration(node)) return node.name.text 138 | 139 | const moduleSpecifier = getModuleSpecifier(node, strict) 140 | return getModuleNameFromSpecifier(moduleSpecifier) 141 | } 142 | 143 | /** 144 | * @param strict - throw an error if node is not part of an import/export node. 145 | * @internal 146 | */ 147 | export function getModuleSpecifier(node: ts.Node, strict = false): ts.Expression | undefined { 148 | // https://github.com/Microsoft/TypeScript/blob/v3.6.4/src/compiler/utilities.ts#L2149 149 | 150 | if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) { 151 | return node.moduleSpecifier 152 | } else if (ts.isImportEqualsDeclaration(node)) { 153 | return ts.isExternalModuleReference(node.moduleReference) ? node.moduleReference.expression : undefined 154 | } else if (ts.isImportSpecifier(node)) { 155 | return node.parent.parent.parent.moduleSpecifier 156 | } else if (ts.isExportSpecifier(node) || ts.isNamespaceImport(node) || ts.isNamedImports(node)) { 157 | return node.parent.parent.moduleSpecifier 158 | } else if (ts.isImportClause(node) || ts.isNamedExports(node)) { 159 | return node.parent.moduleSpecifier 160 | } else if (ts.isImportTypeNode(node)) { 161 | return ts.isLiteralTypeNode(node.argument) ? node.argument.literal : undefined 162 | } else if (node.parent && ts.isImportTypeNode(node.parent)) { 163 | if (ts.isLiteralTypeNode(node)) { 164 | return node.literal 165 | } else if (ts.isEntityName(node)) { 166 | const { argument } = node.parent as ts.ImportTypeNode 167 | return ts.isLiteralTypeNode(argument) ? argument.literal : undefined 168 | } else { 169 | return undefined 170 | } 171 | } else { 172 | if (strict) { 173 | throw Error(`${node.getText()} (kind: ${node.kind}) is not part of an import or export declaration`) 174 | } 175 | } 176 | } 177 | 178 | /** 179 | * @internal 180 | */ 181 | export function getModuleNameFromSpecifier(moduleSpecifier: ts.Expression | undefined): string | undefined { 182 | if (moduleSpecifier && ts.isStringLiteralLike(moduleSpecifier)) { 183 | return ts.getTextOfIdentifierOrLiteral(moduleSpecifier) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/clean-addon.ts: -------------------------------------------------------------------------------- 1 | import { relativeToCWD, parentPaths } from './utils/path' 2 | import { rmrf } from './utils/fs' 3 | import { normalize } from 'path' 4 | import { Color } from './utils/log' 5 | 6 | /** 7 | * Delete files and folders created by a previous build. 8 | * @internal 9 | */ 10 | export default function cleanTargets(targets: string[]) { 11 | if (!targets.length) return 12 | 13 | for (const target of targets) { 14 | console.log('Clean:', relativeToCWD(target)) 15 | rmrf(target) 16 | } 17 | 18 | // Pause on windows to try to work around eventual lingering file handles 19 | if (process.platform === 'win32') { 20 | const until = Date.now() + 500 21 | while (Date.now() < until) {} 22 | } 23 | } 24 | 25 | /** 26 | * @internal 27 | */ 28 | export function protectSensitiveFolders(targets: string[], rootDir: string | undefined, basePath: string | undefined) { 29 | const cwd = process.cwd() 30 | let protectedDirs: string[] = [cwd, ...parentPaths(cwd)] 31 | 32 | if (basePath && basePath !== cwd) { 33 | protectedDirs.push(basePath, ...parentPaths(basePath)) 34 | } 35 | 36 | // compilerOptions properties returns unix separators in windows paths so we must normalize 37 | rootDir = rootDir ? normalize(rootDir) : undefined 38 | if (rootDir && rootDir !== cwd && rootDir !== basePath) { 39 | protectedDirs.push(rootDir, ...parentPaths(rootDir)) 40 | } 41 | 42 | // Dedupe 43 | protectedDirs = [...new Set(protectedDirs)] 44 | 45 | for (const target of targets) { 46 | for (const dir of protectedDirs) { 47 | if (target === dir) { 48 | throw Color.red(`You cannot delete ${target}`) 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/copy-addon.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import { changeDir } from './utils/path' 3 | import { cp } from './utils/fs' 4 | import { Color } from './utils/log' 5 | 6 | /** 7 | * Copy non-typescript files to `outDir`. 8 | * @internal 9 | */ 10 | export default function copyOtherFiles(program: ts.Program) { 11 | const srcDir = program.getCommonSourceDirectory() 12 | 13 | const options = program.getCompilerOptions() 14 | const outDir = options.outDir! // already checked before 15 | const declarationDir = options.declarationDir 16 | 17 | // https://github.com/Microsoft/TypeScript/issues/1863 18 | const excludes: string[] = (program as any)[excludeKey] || [] 19 | // Exclude typescript files and outDir/declarationDir if previously emitted in the same folder 20 | excludes.push('**/*.ts', outDir, declarationDir || '') 21 | const otherFiles = matchAllFilesBut(srcDir, excludes) 22 | 23 | // Track copied files to list them later if needed 24 | const copiedFiles: string[] = [] 25 | 26 | for (const srcOtherFile of otherFiles) { 27 | const destOtherFile = changeDir(srcOtherFile, srcDir, outDir) 28 | 29 | try { 30 | cp(srcOtherFile, destOtherFile) 31 | } catch (error: any) { 32 | if (error.code === 'EEXIST') console.warn(Color.yellow(error.message)) 33 | else throw error 34 | } 35 | 36 | copiedFiles.push(destOtherFile) 37 | } 38 | 39 | return copiedFiles 40 | } 41 | 42 | /** 43 | * Attach exclude pattern to the program during creation, 44 | * to keep a reference when copying other files. 45 | * @internal 46 | */ 47 | export const excludeKey = Symbol('exclude') 48 | 49 | /** 50 | * @internal 51 | */ 52 | function matchAllFilesBut(path: string, excludes: string[] | undefined) { 53 | return ts.sys.readDirectory(path, undefined, excludes, undefined) 54 | } 55 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './builder' 2 | export * from './interfaces' 3 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | 3 | /** 4 | * @public 5 | */ 6 | export type BuildOptions = CreateProgramFromConfigOptions & EmitOptions 7 | 8 | /** 9 | * @public 10 | */ 11 | export interface CreateProgramFromConfigOptions extends TsConfig { 12 | /** 13 | * A root directory to resolve relative path entries in the config file to. 14 | */ 15 | basePath: string 16 | 17 | /** 18 | * Config file to look for and inherit from. 19 | * Either an absolute path or relative to `basePath`. 20 | */ 21 | configFilePath?: string 22 | 23 | /** 24 | * Custom CompilerHost to be used by the Program to interact with the underlying system. 25 | */ 26 | host?: ts.CompilerHost 27 | } 28 | 29 | /** 30 | * @public 31 | */ 32 | export interface EmitOptions { 33 | /** 34 | * Copy non typescript files and folders to `outDir` path if defined in compiler options. 35 | */ 36 | copyOtherToOutDir?: boolean 37 | 38 | /** 39 | * List of files or folders to recursively delete before compilation : 40 | * accepts absolute paths or relative to `basePath`. 41 | * 42 | * Also accepts a map of common compiler options targets. 43 | */ 44 | clean?: string[] | { outDir?: true; outFile?: true; declarationDir?: true } 45 | 46 | /** 47 | * A root directory to resolve relative paths in the `clean` option to. 48 | */ 49 | basePath?: string 50 | 51 | /** 52 | * Option to bundle .d.ts files from one or several entrypoints. 53 | */ 54 | bundleDeclaration?: EmitOptions.Bundle 55 | } 56 | 57 | export namespace EmitOptions { 58 | export interface Bundle { 59 | /** 60 | * Specifies the .ts file to be used as the starting point for analysis and the final bundle. 61 | * Path is relative to the output directory. 62 | */ 63 | entryPoint: string | string[] 64 | 65 | /** 66 | * If any error happens during bundling, fallback to original declaration files. 67 | * 68 | * Switch to `false` to disable this behavior. 69 | * @default true 70 | */ 71 | fallbackOnError?: boolean 72 | 73 | /** 74 | * Keep global declarations in the bundle, e.g. 75 | * ```ts 76 | * declare global { 77 | * interface IGlobal {} 78 | * } 79 | * ``` 80 | * 81 | * Switch to `false` to disable this behavior. 82 | * @default true 83 | */ 84 | globals?: boolean 85 | 86 | /** 87 | * Keep external library augmentations in the bundle, e.g. 88 | * ```ts 89 | * declare module 'library' { 90 | * interface IMerged {} 91 | * } 92 | * ``` 93 | * 94 | * Switch to `false` to disable this behavior. 95 | * @default true 96 | */ 97 | augmentations?: boolean 98 | 99 | /** 100 | * Adds declarations to the final bundle, if they do not already exist. 101 | */ 102 | extras?: EmitOptions.Bundle.Extra[] 103 | } 104 | 105 | export namespace Bundle { 106 | export interface Extra { 107 | position: 'after-imports' | 'after-exports' 108 | declaration: string 109 | } 110 | } 111 | } 112 | 113 | /** 114 | * Mimicks part of `tsconfig.json` file. 115 | * 116 | * Options like `compileOnSave`, `typeAcquisition`, `watch` are not implemented, 117 | * since this is for a basic build pipeline. 118 | * 119 | * Retrieved with the help of: 120 | * - https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/tsconfig.json 121 | * - https://app.quicktype.io?share=K02ozbca8ED63VSPHjP1 122 | * 123 | * @public 124 | */ 125 | export interface TsConfig { 126 | /** 127 | * Instructs the TypeScript compiler how to compile .ts files. 128 | */ 129 | compilerOptions?: TsConfigCompilerOptions 130 | 131 | /** 132 | * Specifies a list of glob patterns that match files to be included in compilation. 133 | * If no 'files' or 'include' property is present in a tsconfig.json, the compiler 134 | * defaults to including all files in the containing directory and subdirectories 135 | * except those specified by 'exclude'. 136 | */ 137 | include?: string[] 138 | 139 | /** 140 | * Specifies a list of files to be excluded from compilation. 141 | * The 'exclude' property only affects the files included via the 'include' property 142 | * and not the 'files' property. 143 | */ 144 | exclude?: string[] 145 | 146 | /** 147 | * If no 'files' or 'include' property is present in a tsconfig.json, the compiler 148 | * defaults to including all files in the containing directory and subdirectories 149 | * except those specified by 'exclude'. When a 'files' property is specified, 150 | * only those files and those specified by 'include' are included. 151 | */ 152 | files?: string[] 153 | 154 | /** 155 | * Path to base configuration file to inherit from. 156 | */ 157 | extends?: string 158 | 159 | /** 160 | * Referenced projects. 161 | */ 162 | references?: Array<{ 163 | /** Path to referenced tsconfig or to folder containing tsconfig */ 164 | path: string 165 | }> 166 | } 167 | 168 | /** 169 | * Instructs the TypeScript compiler how to compile .ts files. 170 | * 171 | * @public 172 | */ 173 | export interface TsConfigCompilerOptions { 174 | /** 175 | * Allow javascript files to be compiled. Requires TypeScript version 1.8 or later. 176 | */ 177 | allowJs?: boolean 178 | /** 179 | * Allow default imports from modules with no default export. This does not affect code 180 | * emit, just typechecking. Requires TypeScript version 1.8 or later. 181 | */ 182 | allowSyntheticDefaultImports?: boolean 183 | /** 184 | * Allow accessing UMD globals from modules. Requires TypeScript version 3.5 or later. 185 | */ 186 | allowUmdGlobalAccess?: boolean 187 | /** 188 | * Do not report errors on unreachable code. Requires TypeScript version 1.8 or later. 189 | */ 190 | allowUnreachableCode?: boolean 191 | /** 192 | * Do not report errors on unused labels. Requires TypeScript version 1.8 or later. 193 | */ 194 | allowUnusedLabels?: boolean 195 | /** 196 | * Parse in strict mode and emit 'use strict' for each source file. Requires TypeScript 197 | * version 2.1 or later. 198 | */ 199 | alwaysStrict?: boolean 200 | /** 201 | * Have recompiles in '--incremental' and '--watch' assume that changes within a file will 202 | * only affect files directly depending on it. Requires TypeScript version 3.8 or later. 203 | */ 204 | // assumeChangesOnlyAffectDirectDependencies?: boolean; 205 | /** 206 | * Base directory to resolve non-relative module names. 207 | */ 208 | baseUrl?: string 209 | /** 210 | * The character set of the input files. This setting is deprecated. 211 | */ 212 | charset?: string 213 | /** 214 | * Report errors in .js files. Requires TypeScript version 2.3 or later. 215 | */ 216 | checkJs?: boolean 217 | /** 218 | * Enables building for project references. Requires TypeScript version 3.0 or later. 219 | */ 220 | composite?: boolean 221 | /** 222 | * Generates corresponding d.ts files. 223 | */ 224 | declaration?: boolean 225 | /** 226 | * Specify output directory for generated declaration files. Requires TypeScript version 2.0 227 | * or later. 228 | */ 229 | declarationDir?: string 230 | /** 231 | * Generates a sourcemap for each corresponding '.d.ts' file. Requires TypeScript version 232 | * 2.9 or later. 233 | */ 234 | declarationMap?: boolean 235 | /** 236 | * Show diagnostic information. This setting is deprecated. See `extendedDiagnostics.` 237 | */ 238 | diagnostics?: boolean 239 | /** 240 | * Recommend IDE's to load referenced composite projects dynamically instead of loading them 241 | * all immediately. Requires TypeScript version 4.0 or later. 242 | */ 243 | disableReferencedProjectLoad?: boolean 244 | /** 245 | * Disable size limit for JavaScript project. Requires TypeScript version 2.0 or later. 246 | */ 247 | disableSizeLimit?: boolean 248 | /** 249 | * Disable solution searching for this project. Requires TypeScript version 3.8 or later. 250 | */ 251 | disableSolutionSearching?: boolean 252 | /** 253 | * Provide full support for iterables in 'for-of', spread, and destructuring when targeting 254 | * 'ES5' or 'ES3'. Requires TypeScript version 2.3 or later. 255 | */ 256 | downlevelIteration?: boolean 257 | /** 258 | * Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. 259 | */ 260 | emitBOM?: boolean 261 | /** 262 | * Only emit '.d.ts' declaration files. Requires TypeScript version 2.8 or later. 263 | */ 264 | emitDeclarationOnly?: boolean 265 | /** 266 | * Emit design-type metadata for decorated declarations in source. 267 | */ 268 | emitDecoratorMetadata?: boolean 269 | /** 270 | * Emit '__importStar' and '__importDefault' helpers for runtime babel ecosystem 271 | * compatibility and enable '--allowSyntheticDefaultImports' for typesystem compatibility. 272 | * Requires TypeScript version 2.7 or later. 273 | */ 274 | esModuleInterop?: boolean 275 | /** 276 | * Enables experimental support for ES7 decorators. 277 | */ 278 | experimentalDecorators?: boolean 279 | /** 280 | * Show verbose diagnostic information. 281 | */ 282 | extendedDiagnostics?: boolean 283 | /** 284 | * Specify the polling strategy to use when the system runs out of or doesn't support native 285 | * file watchers. Requires TypeScript version 3.8 or later. 286 | */ 287 | // fallbackPolling?: 'dynamicPriorityPolling' | 'fixedPollingInterval' | 'priorityPollingInterval' 288 | /** 289 | * Disallow inconsistently-cased references to the same file. Enabling this setting is 290 | * recommended. 291 | */ 292 | forceConsistentCasingInFileNames?: boolean 293 | /** 294 | * Emit a v8 CPI profile during the compiler run, which may provide insight into slow 295 | * builds. Requires TypeScript version 3.7 or later. 296 | */ 297 | // generateCpuProfile?: string 298 | /** 299 | * Import emit helpers (e.g. '__extends', '__rest', etc..) from tslib. Requires TypeScript 300 | * version 2.1 or later. 301 | */ 302 | importHelpers?: boolean 303 | /** 304 | * This flag controls how imports work. When set to `remove`, imports that only reference 305 | * types are dropped. When set to `preserve`, imports are never dropped. When set to 306 | * `error`, imports that can be replaced with `import type` will result in a compiler error. 307 | * Requires TypeScript version 3.8 or later. 308 | */ 309 | importsNotUsedAsValues?: 'error' | 'preserve' | 'remove' 310 | /** 311 | * Enable incremental compilation. Requires TypeScript version 3.4 or later. 312 | */ 313 | incremental?: boolean 314 | /** 315 | * Emit a single file with source maps instead of having a separate file. Requires 316 | * TypeScript version 1.5 or later. 317 | */ 318 | inlineSourceMap?: boolean 319 | /** 320 | * Emit the source alongside the sourcemaps within a single file; requires --inlineSourceMap 321 | * to be set. Requires TypeScript version 1.5 or later. 322 | */ 323 | inlineSources?: boolean 324 | /** 325 | * Unconditionally emit imports for unresolved files. 326 | */ 327 | isolatedModules?: boolean 328 | /** 329 | * Specify JSX code generation: 'preserve', 'react', 'react-jsx', 'react-jsxdev' 330 | * or'react-native'. Requires TypeScript version 2.2 or later. 331 | */ 332 | jsx?: 'preserve' | 'react' | 'react-native' 333 | /** 334 | * Specify the JSX factory function to use when targeting react JSX emit, e.g. 335 | * 'React.createElement' or 'h'. Requires TypeScript version 2.1 or later. 336 | */ 337 | jsxFactory?: string 338 | /** 339 | * Specify the JSX Fragment reference to use for fragements when targeting react JSX emit, 340 | * e.g. 'React.Fragment' or 'Fragment'. Requires TypeScript version 4.0 or later. 341 | */ 342 | jsxFragmentFactory?: string 343 | /** 344 | * Declare the module specifier to be used for importing the `jsx` and `jsxs` factory 345 | * functions when using jsx as "react-jsx" or "react-jsxdev". Requires TypeScript version 346 | * 4.1 or later. 347 | */ 348 | jsxImportSource?: string 349 | /** 350 | * Resolve 'keyof' to string valued property names only (no numbers or symbols). This 351 | * setting is deprecated. Requires TypeScript version 2.9 or later. 352 | */ 353 | keyofStringsOnly?: boolean 354 | /** 355 | * Specify library file to be included in the compilation. Requires TypeScript version 2.0 356 | * or later. 357 | */ 358 | lib?: Array< 359 | | 'dom' 360 | | 'dom.iterable' 361 | | 'es2015' 362 | | 'es2015.collection' 363 | | 'es2015.core' 364 | | 'es2015.generator' 365 | | 'es2015.iterable' 366 | | 'es2015.promise' 367 | | 'es2015.proxy' 368 | | 'es2015.reflect' 369 | | 'es2015.symbol' 370 | | 'es2015.symbol.wellknown' 371 | | 'es2016' 372 | | 'es2016.array.include' 373 | | 'es2017' 374 | | 'es2017.intl' 375 | | 'es2017.object' 376 | | 'es2017.sharedmemory' 377 | | 'es2017.string' 378 | | 'es2017.typedarrays' 379 | | 'es2018' 380 | | 'es2018.asynciterable' 381 | | 'es2018.intl' 382 | | 'es2018.promise' 383 | | 'es2018.regexp' 384 | | 'es2019' 385 | | 'es2019.array' 386 | | 'es2019.object' 387 | | 'es2019.string' 388 | | 'es2019.symbol' 389 | | 'es2020' 390 | | 'es2020.string' 391 | | 'es2020.symbol.wellknown' 392 | | 'es5' 393 | | 'es6' 394 | | 'es7' 395 | | 'esnext' 396 | | 'esnext.array' 397 | | 'esnext.asynciterable' 398 | | 'esnext.bigint' 399 | | 'esnext.intl' 400 | | 'esnext.symbol' 401 | | 'scripthost' 402 | | 'webworker' 403 | > 404 | /** 405 | * Enable to list all emitted files. Requires TypeScript version 2.0 or later. 406 | */ 407 | listEmittedFiles?: boolean 408 | /** 409 | * Print names of files part of the compilation. 410 | */ 411 | listFiles?: boolean 412 | /** 413 | * Print names of files that are part of the compilation and then stop processing. 414 | */ 415 | // listFilesOnly?: boolean 416 | /** 417 | * Specifies the location where debugger should locate map files instead of generated 418 | * locations 419 | */ 420 | mapRoot?: string 421 | /** 422 | * The maximum dependency depth to search under node_modules and load JavaScript files. Only 423 | * applicable with --allowJs. 424 | */ 425 | maxNodeModuleJsDepth?: number 426 | /** 427 | * Specify module code generation. 428 | * Only 'AMD' and 'System' can be used in conjunction with --outFile. 429 | */ 430 | module?: 'none' | 'commonjs' | 'amd' | 'system' | 'umd' | 'es6' | 'es2015' | 'es2020' | 'esnext' 431 | /** 432 | * Specifies module resolution strategy: 'node' (Node) or 'classic' (TypeScript pre 1.6) . 433 | */ 434 | moduleResolution?: 'node' | 'classic' 435 | /** 436 | * Specifies the end of line sequence to be used when emitting files: 'CRLF' (Windows) or 437 | * 'LF' (Unix). Requires TypeScript version 1.5 or later. 438 | */ 439 | newLine?: 'CRLF' | 'LF' 440 | /** 441 | * Do not emit output. 442 | */ 443 | noEmit?: boolean 444 | /** 445 | * Do not generate custom helper functions like __extends in compiled output. Requires 446 | * TypeScript version 1.5 or later. 447 | */ 448 | noEmitHelpers?: boolean 449 | /** 450 | * Do not emit outputs if any type checking errors were reported. Requires TypeScript 451 | * version 1.4 or later. 452 | */ 453 | noEmitOnError?: boolean 454 | /** 455 | * Do not truncate error messages. This setting is deprecated. 456 | */ 457 | noErrorTruncation?: boolean 458 | /** 459 | * Report errors for fallthrough cases in switch statement. Requires TypeScript version 1.8 460 | * or later. 461 | */ 462 | noFallthroughCasesInSwitch?: boolean 463 | /** 464 | * Warn on expressions and declarations with an implied 'any' type. Enabling this setting is 465 | * recommended. 466 | */ 467 | noImplicitAny?: boolean 468 | /** 469 | * Report error when not all code paths in function return a value. Requires TypeScript 470 | * version 1.8 or later. 471 | */ 472 | noImplicitReturns?: boolean 473 | /** 474 | * Raise error on 'this' expressions with an implied any type. Enabling this setting is 475 | * recommended. Requires TypeScript version 2.0 or later. 476 | */ 477 | noImplicitThis?: boolean 478 | /** 479 | * Do not emit 'use strict' directives in module output. 480 | */ 481 | noImplicitUseStrict?: boolean 482 | /** 483 | * Do not include the default library file (lib.d.ts). 484 | */ 485 | noLib?: boolean 486 | /** 487 | * Do not add triple-slash references or module import targets to the list of compiled files. 488 | */ 489 | noResolve?: boolean 490 | /** 491 | * Disable strict checking of generic signatures in function types. Requires TypeScript 492 | * version 2.4 or later. 493 | */ 494 | noStrictGenericChecks?: boolean 495 | /** 496 | * Add `undefined` to an un-declared field in a type. Requires TypeScript version 4.1 or 497 | * later. 498 | */ 499 | noUncheckedIndexedAccess?: boolean 500 | /** 501 | * Report errors on unused locals. Requires TypeScript version 2.0 or later. 502 | */ 503 | noUnusedLocals?: boolean 504 | /** 505 | * Report errors on unused parameters. Requires TypeScript version 2.0 or later. 506 | */ 507 | noUnusedParameters?: boolean 508 | /** 509 | * Redirect output structure to the directory. 510 | */ 511 | outDir?: string 512 | /** 513 | * Concatenate and emit output to single file. 514 | */ 515 | outFile?: string 516 | /** 517 | * Specify path mapping to be computed relative to baseUrl option. 518 | */ 519 | paths?: { [key: string]: string[] } 520 | /** 521 | * List of TypeScript language server plugins to load. Requires TypeScript version 2.3 or 522 | * later. 523 | */ 524 | plugins?: Array<{ 525 | /** Plugin name. */ 526 | name?: string 527 | }> 528 | /** 529 | * Do not erase const enum declarations in generated code. 530 | */ 531 | preserveConstEnums?: boolean 532 | /** 533 | * Do not resolve symlinks to their real path; treat a symlinked file like a real one. 534 | */ 535 | preserveSymlinks?: boolean 536 | /** 537 | * Keep outdated console output in watch mode instead of clearing the screen. 538 | */ 539 | // preserveWatchOutput?: boolean 540 | /** 541 | * Stylize errors and messages using color and context (experimental). 542 | */ 543 | pretty?: boolean 544 | /** 545 | * Specifies the object invoked for createElement and __spread when targeting 'react' JSX emit. 546 | */ 547 | reactNamespace?: string 548 | /** 549 | * Do not emit comments to output. 550 | */ 551 | removeComments?: boolean 552 | /** 553 | * Include modules imported with '.json' extension. Requires TypeScript version 2.9 or later. 554 | */ 555 | resolveJsonModule?: boolean 556 | /** 557 | * Specifies the root directory of input files. Use to control the output directory 558 | * structure with --outDir. 559 | */ 560 | rootDir?: string 561 | /** 562 | * Specify list of root directories to be used when resolving modules. Requires TypeScript 563 | * version 2.0 or later. 564 | */ 565 | rootDirs?: string[] 566 | /** 567 | * Use `skipLibCheck` instead. Skip type checking of default library declaration files. 568 | */ 569 | skipDefaultLibCheck?: boolean 570 | /** 571 | * Skip type checking of declaration files. Enabling this setting is recommended. Requires 572 | * TypeScript version 2.0 or later. 573 | */ 574 | skipLibCheck?: boolean 575 | /** 576 | * Generates corresponding '.map' file. 577 | */ 578 | sourceMap?: boolean 579 | /** 580 | * Specifies the location where debugger should locate TypeScript files instead of source 581 | * locations. 582 | */ 583 | sourceRoot?: string 584 | /** 585 | * Enable all strict type checking options. Enabling this setting is recommended. Requires 586 | * TypeScript version 2.3 or later. 587 | */ 588 | strict?: boolean 589 | /** 590 | * Enable stricter checking of of the `bind`, `call`, and `apply` methods on functions. 591 | * Enabling this setting is recommended. Requires TypeScript version 3.2 or later. 592 | */ 593 | strictBindCallApply?: boolean 594 | /** 595 | * Disable bivariant parameter checking for function types. Enabling this setting is 596 | * recommended. Requires TypeScript version 2.6 or later. 597 | */ 598 | strictFunctionTypes?: boolean 599 | /** 600 | * Enable strict null checks. Enabling this setting is recommended. Requires TypeScript 601 | * version 2.0 or later. 602 | */ 603 | strictNullChecks?: boolean 604 | /** 605 | * Ensure non-undefined class properties are initialized in the constructor. Enabling this 606 | * setting is recommended. Requires TypeScript version 2.7 or later. 607 | */ 608 | strictPropertyInitialization?: boolean 609 | /** 610 | * Do not emit declarations for code that has an '@internal' annotation. 611 | */ 612 | stripInternal?: boolean 613 | /** 614 | * Suppress excess property checks for object literals. It is recommended to use @ts-ignore 615 | * comments instead of enabling this setting. 616 | */ 617 | suppressExcessPropertyErrors?: boolean 618 | /** 619 | * Suppress noImplicitAny errors for indexing objects lacking index signatures. It is 620 | * recommended to use @ts-ignore comments instead of enabling this setting. 621 | */ 622 | suppressImplicitAnyIndexErrors?: boolean 623 | /** 624 | * Specify ECMAScript target version. 625 | */ 626 | target?: 'es3' | 'es5' | 'es6' | 'es2015' | 'es2016' | 'es2017' | 'es2018' | 'es2019' | 'es2020' | 'esnext' 627 | /** 628 | * Enable tracing of the name resolution process. Requires TypeScript version 2.0 or later. 629 | */ 630 | traceResolution?: boolean 631 | /** 632 | * Specify file to store incremental compilation information. Requires TypeScript version 633 | * 3.4 or later. 634 | */ 635 | tsBuildInfoFile?: string 636 | /** 637 | * Specify list of directories for type definition files to be included. Requires TypeScript 638 | * version 2.0 or later. 639 | */ 640 | typeRoots?: string[] 641 | /** 642 | * Type declaration files to be included in compilation. Requires TypeScript version 2.0 or 643 | * later. 644 | */ 645 | types?: string[] 646 | /** 647 | * Emit ECMAScript standard class fields. Requires TypeScript version 3.7 or later. 648 | */ 649 | useDefineForClassFields?: boolean 650 | /** 651 | * Watch input files. 652 | */ 653 | // watch?: boolean 654 | /** 655 | * Specify the strategy for watching directories under systems that lack recursive 656 | * file-watching functionality. Requires TypeScript version 3.8 or later. 657 | */ 658 | // watchDirectory?: 'dynamicPriorityPolling' | 'fixedPollingInterval' | 'useFsEvents' 659 | /** 660 | * Specify the strategy for watching individual files. Requires TypeScript version 3.8 or 661 | * later. 662 | */ 663 | // watchFile?: 664 | // | 'dynamicPriorityPolling' 665 | // | 'fixedPollingInterval' 666 | // | 'priorityPollingInterval' 667 | // | 'useFsEvents' 668 | // | 'useFsEventsOnParentDirectory' 669 | } 670 | -------------------------------------------------------------------------------- /src/ts-internals.d.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | 3 | /** 4 | * Expose some internal typescript utilities. Probably not a good idea 5 | * since internal api can change without notice, but it's either that or duplicating. 6 | */ 7 | declare module 'typescript' { 8 | /** 9 | * @see https://github.com/microsoft/TypeScript/blob/v3.5.3/src/compiler/utilities.ts#L7087-L7105 10 | * @internal 11 | */ 12 | function createCompilerDiagnostic(message: DiagnosticMessage): Diagnostic 13 | 14 | /** 15 | * @see https://github.com/microsoft/TypeScript/blob/v3.5.3/src/compiler/utilities.ts#L7107-L7117 16 | * @internal 17 | */ 18 | function createCompilerDiagnosticFromMessageChain(chain: DiagnosticMessageChain): Diagnostic 19 | 20 | /** 21 | * @see https://github.com/microsoft/TypeScript/blob/v3.5.3/src/compiler/utilities.ts#L3844-L3846 22 | * @internal 23 | */ 24 | function hasModifier(node: Node, flags: ModifierFlags): boolean 25 | 26 | /** 27 | * Retrieves the (cached) module resolution information for a module name that was exported from a SourceFile. 28 | * The compiler populates this cache as part of analyzing the source file. 29 | * @see https://github.com/Microsoft/TypeScript/blob/v3.5.3/src/compiler/utilities.ts#L223-L225 30 | */ 31 | function getResolvedModule(sourceFile: SourceFile, moduleNameText: string): ResolvedModuleFull | undefined 32 | 33 | /** 34 | * https://github.com/microsoft/TypeScript/blob/v3.6.4/src/compiler/utilities.ts#L46-L54 35 | * @internal 36 | */ 37 | function createSymbolTable(symbols?: readonly Symbol[]): SymbolTable 38 | 39 | /** 40 | * @see https://github.com/Microsoft/TypeScript/blob/v3.6.4/src/compiler/types.ts#L2244 41 | * @internal 42 | */ 43 | interface AmbientModuleDeclaration extends ModuleDeclaration { 44 | body?: ModuleBlock 45 | } 46 | 47 | /** 48 | * `declare module "name"` | `declare global` 49 | * @see https://github.com/Microsoft/TypeScript/blob/v3.6.4/src/compiler/utilities.ts#L640-L642 50 | * @internal 51 | */ 52 | function isAmbientModule(node: Node): node is AmbientModuleDeclaration 53 | 54 | /** 55 | * `declare global` 56 | * @see https://github.com/Microsoft/TypeScript/blob/v3.6.4/src/compiler/utilities.ts#L678-L680 57 | * @internal 58 | */ 59 | function isGlobalScopeAugmentation(module: ModuleDeclaration): boolean 60 | 61 | /** 62 | * External module augmentation is a ambient module declaration that is either: 63 | * - defined in the top level scope and source file is an external module 64 | * - defined inside ambient module declaration located in the top level scope and source file not an external module 65 | * 66 | * @see https://github.com/Microsoft/TypeScript/blob/v3.6.4/src/compiler/utilities.ts#L682-L684 67 | * @internal 68 | */ 69 | function isExternalModuleAugmentation(node: Node): node is AmbientModuleDeclaration 70 | 71 | /** 72 | * @see https://github.com/Microsoft/TypeScript/blob/v3.6.4/src/compiler/utilities.ts#L1050-L1063 73 | * @internal 74 | */ 75 | function getJSDocCommentRanges(node: Node, text: string): CommentRange[] | undefined 76 | 77 | /** 78 | * @see https://github.com/Microsoft/TypeScript/blob/v3.6.4/src/compiler/utilities.ts#L1003-L1005 79 | * @internal 80 | */ 81 | function isJsonSourceFile(file: SourceFile): file is JsonSourceFile 82 | 83 | /** 84 | * @see https://github.com/Microsoft/TypeScript/blob/v3.6.4/src/compiler/utilities.ts#L2834-L2836 85 | * @internal 86 | */ 87 | function getTextOfIdentifierOrLiteral(node: PropertyNameLiteral): string 88 | 89 | /** 90 | * @see https://github.com/Microsoft/TypeScript/blob/v3.6.4/src/compiler/utilities.ts#L5226-L5228 91 | * @internal 92 | */ 93 | function isNamedDeclaration(node: Node): node is NamedDeclaration & { name: DeclarationName } 94 | 95 | /** 96 | * @see https://github.com/Microsoft/TypeScript/blob/v3.6.4/src/compiler/utilities.ts#L7735-L7737 97 | * @internal 98 | */ 99 | function getRelativePathFromFile(from: string, to: string, getCanonicalFileName: (fileName: string) => string): string 100 | 101 | interface CompilerOptions { 102 | listFiles?: boolean 103 | listEmittedFiles?: boolean 104 | pretty?: boolean 105 | } 106 | 107 | interface Program { 108 | /** 109 | * @see https://github.com/microsoft/TypeScript/blob/v3.6.4/src/compiler/program.ts#L984-L1008 110 | * @internal 111 | */ 112 | getCommonSourceDirectory: () => string 113 | 114 | /** 115 | * @see https://github.com/microsoft/TypeScript/blob/v3.6.4/src/compiler/types.ts#L2998 116 | * @internal 117 | */ 118 | getResolvedTypeReferenceDirectives(): Map 119 | 120 | /** 121 | * @see https://github.com/microsoft/TypeScript/blob/v3.6.4/src/compiler/program.ts#L1514-L1516 122 | * @internal 123 | */ 124 | getDiagnosticsProducingTypeChecker: () => TypeChecker 125 | 126 | /** 127 | * Given a source file, get the name of the package it was imported from. 128 | * @see https://github.com/microsoft/TypeScript/blob/v3.6.4/src/compiler/types.ts#L3009 129 | * @internal 130 | */ 131 | sourceFileToPackageName: Map 132 | } 133 | 134 | interface TypeChecker { 135 | /** 136 | * @see https://raw.githubusercontent.com/Microsoft/TypeScript/v3.6.4/src/compiler/checker.ts 137 | * @internal 138 | */ 139 | getImmediateAliasedSymbol(symbol: Symbol): Symbol | undefined 140 | 141 | /** 142 | * @see https://github.com/microsoft/TypeScript/blob/v3.6.4/src/compiler/types.ts#L3292 143 | * @see https://raw.githubusercontent.com/Microsoft/TypeScript/v3.6.4/src/compiler/checker.ts 144 | * @internal 145 | */ 146 | getEmitResolver: (sourceFile?: SourceFile, cancellationToken?: CancellationToken) => EmitResolver 147 | } 148 | 149 | /** 150 | * @see https://github.com/microsoft/TypeScript/blob/v3.6.4/src/compiler/types.ts#L3605-L3644 151 | * @internal 152 | */ 153 | interface EmitResolver { 154 | hasGlobalName(name: string): boolean 155 | } 156 | 157 | interface Node { 158 | /** 159 | * Locals associated with node (initialized by binding). 160 | * @see https://github.com/microsoft/TypeScript/blob/v3.6.4/src/compiler/types.ts#L633 161 | * @internal 162 | */ 163 | locals?: SymbolTable 164 | 165 | /** 166 | * Next container in declaration order (initialized by binding). 167 | * @see https://github.com/microsoft/TypeScript/blob/v3.6.4/src/compiler/types.ts#L634 168 | * @internal 169 | */ 170 | nextContainer?: Node 171 | } 172 | 173 | interface Symbol { 174 | /** 175 | * @see https://github.com/microsoft/TypeScript/blob/v3.6.4/src/compiler/types.ts#L3743 176 | * @internal 177 | */ 178 | parent?: Symbol 179 | } 180 | 181 | interface SourceFile { 182 | /** 183 | * @see https://github.com/microsoft/TypeScript/blob/v3.6.4/src/compiler/types.ts#L2687 184 | * @internal 185 | */ 186 | path: Path 187 | 188 | /** 189 | * Resolved path can be different from path property, 190 | * when file is included through project reference is mapped to its output instead of source, 191 | * in that case resolvedPath = path to output file, path = input file's path. 192 | * 193 | * @see https://github.com/microsoft/TypeScript/blob/v3.6.4/src/compiler/types.ts#L2694 194 | * @internal 195 | */ 196 | resolvedPath: Path 197 | 198 | /** 199 | * @see https://github.com/microsoft/TypeScript/blob/v3.6.4/src/compiler/types.ts#L2745 200 | * @internal 201 | */ 202 | identifiers: Map // Map from a string to an interned string 203 | 204 | /** 205 | * @see https://github.com/microsoft/TypeScript/blob/v3.6.4/src/compiler/types.ts#L2767 206 | * @internal 207 | */ 208 | classifiableNames?: ReadonlyUnderscoreEscapedMap 209 | 210 | /** 211 | * @see https://github.com/microsoft/TypeScript/blob/v3.6.4/src/compiler/types.ts#L2773 212 | * @internal 213 | */ 214 | imports: ReadonlyArray 215 | 216 | /** 217 | * The first "most obvious" node that makes a file an external module. 218 | * This is intended to be the first top-level import/export, 219 | * but could be arbitrarily nested (e.g. `import.meta`). 220 | * 221 | * @see https://github.com/microsoft/TypeScript/blob/v3.6.4/src/compiler/types.ts#L2739 222 | * @internal 223 | */ 224 | externalModuleIndicator?: Node 225 | 226 | /** 227 | * @see https://github.com/microsoft/TypeScript/blob/v3.6.4/src/compiler/types.ts#L2775 228 | * @internal 229 | */ 230 | moduleAugmentations: ReadonlyArray 231 | 232 | /** 233 | * @see https://github.com/microsoft/TypeScript/blob/v3.6.4/src/compiler/types.ts#L2777 234 | * @internal 235 | */ 236 | ambientModuleNames: ReadonlyArray 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/utils/fs.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import { join, dirname } from 'path' 3 | import { relativeToCWD } from './path' 4 | 5 | /** 6 | * Copy file, create parent folders if necessary. 7 | * @internal 8 | */ 9 | export function cp(srcFilePath: string, destFilePath: string, overwrite = false) { 10 | const parentDir = dirname(destFilePath) 11 | fs.mkdirSync(parentDir, { recursive: true }) // no EEXIST error issue with recursive option 12 | fs.copyFileSync(srcFilePath, destFilePath, overwrite ? 0 : fs.constants.COPYFILE_EXCL) 13 | } 14 | 15 | /** 16 | * Delete file/folder recursively. 17 | * @internal 18 | */ 19 | export function rmrf(path: string) { 20 | // TODO: rely on ENOENT instead 21 | // https://nodejs.org/dist/latest/docs/api/fs.html#fs_fs_exists_path_callback 22 | if (!fs.existsSync(path)) return 23 | 24 | if (tryIsDirectory(path)) deleteRecursive(path) 25 | else tryDeleteFile(path) 26 | 27 | function deleteRecursive(dirPath: string) { 28 | const paths = readdirPaths(dirPath) 29 | 30 | for (const path_ of paths) { 31 | if (tryIsDirectory(path_)) deleteRecursive(path_) 32 | else tryDeleteFile(path_) 33 | } 34 | 35 | tryDeleteEmptyDir(dirPath) 36 | } 37 | } 38 | 39 | /** 40 | * `readdir` with full paths instead of names. 41 | * @internal 42 | */ 43 | export function readdirPaths(dir: string) { 44 | return fs.readdirSync(dir).map((name) => join(dir, name)) 45 | } 46 | 47 | /** 48 | * @internal 49 | */ 50 | function tryIsDirectory(path: string): boolean { 51 | const stats = fsSyncRetry(fs.lstatSync, path, ['EPERM'], 2) 52 | return stats!.isDirectory() 53 | } 54 | 55 | /** 56 | * @internal 57 | */ 58 | function tryDeleteFile(filePath: string) { 59 | fsSyncRetry(fs.unlinkSync, filePath, ['EBUSY', 'EPERM']) 60 | } 61 | 62 | /** 63 | * @internal 64 | */ 65 | function tryDeleteEmptyDir(dirPath: string) { 66 | fsSyncRetry(fs.rmdirSync, dirPath, ['EBUSY', 'EPERM', 'ENOTEMPTY']) 67 | } 68 | 69 | /** 70 | * Retry fs sync operations on some error codes, to bypass locks (especially on windows). 71 | * Inspired from https://github.com/isaacs/rimraf 72 | * @internal 73 | */ 74 | function fsSyncRetry any>( 75 | fsSyncFn: T, 76 | path: string, 77 | errorCodes: ('EBUSY' | 'EPERM' | 'ENOTEMPTY' | 'EMFILE')[], 78 | tries = 13 79 | ): ReturnType { 80 | let round = 1 81 | 82 | do { 83 | if (round > 4) { 84 | console.log(`Try ${fsSyncFn.name} on ${relativeToCWD(path)} for the ${round}th time (out of ${tries})`) 85 | } 86 | 87 | try { 88 | return fsSyncFn(path) 89 | } catch (err: any) { 90 | if (!errorCodes.includes(err.code) || round > tries) throw err 91 | 92 | fixWindowsEPERM(path) 93 | pause(round * 100) 94 | round++ 95 | } 96 | } while (true) 97 | 98 | function pause(ms: number) { 99 | const until = Date.now() + ms 100 | while (Date.now() < until) {} 101 | } 102 | 103 | function fixWindowsEPERM(path_: string) { 104 | if (process.platform === 'win32') fs.chmodSync(path_, 0o666) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/utils/log.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | 3 | /** 4 | * Log compiler diagnostics to stderr. 5 | * @internal 6 | */ 7 | export function logDiagnostics(diagnostics: ts.Diagnostic[], pretty = false) { 8 | if (!diagnostics.length) return 9 | 10 | const formatHost: ts.FormatDiagnosticsHost = { 11 | getCanonicalFileName: (path) => path, 12 | getCurrentDirectory: ts.sys.getCurrentDirectory, 13 | getNewLine: () => ts.sys.newLine, 14 | } 15 | 16 | const message = pretty 17 | ? ts.formatDiagnosticsWithColorAndContext(diagnostics, formatHost) 18 | : ts.formatDiagnostics(diagnostics, formatHost) 19 | 20 | console.warn(message) 21 | } 22 | 23 | /** 24 | * @internal 25 | */ 26 | export namespace Color { 27 | const isTTY = !!ts.sys.writeOutputIsTTY && ts.sys.writeOutputIsTTY() 28 | 29 | export const grey = (text: string) => (isTTY ? EscSeq.Grey + text + EscSeq.Reset : text) 30 | 31 | export const red = (text: string) => (isTTY ? EscSeq.Red + text + EscSeq.Reset : text) 32 | 33 | export const green = (text: string) => (isTTY ? EscSeq.Green + text + EscSeq.Reset : text) 34 | 35 | export const yellow = (text: string) => (isTTY ? EscSeq.Yellow + text + EscSeq.Reset : text) 36 | 37 | export const blue = (text: string) => (isTTY ? EscSeq.Blue + text + EscSeq.Reset : text) 38 | 39 | export const magenta = (text: string) => (isTTY ? EscSeq.Magenta + text + EscSeq.Reset : text) 40 | 41 | export const cyan = (text: string) => (isTTY ? EscSeq.Cyan + text + EscSeq.Reset : text) 42 | 43 | /** 44 | * Foreground color escape sequences. 45 | * Taken from https://github.com/microsoft/TypeScript/blob/v3.5.3/src/compiler/program.ts#L368 46 | */ 47 | enum EscSeq { 48 | Grey = '\u001b[90m', 49 | Red = '\u001b[91m', 50 | Green = '\u001b[92m', 51 | Yellow = '\u001b[93m', 52 | Blue = '\u001b[94m', 53 | Magenta = '\u001b[95m', 54 | Cyan = '\u001b[96m', 55 | Reset = '\u001b[0m', 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/utils/manipulation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @internal 3 | */ 4 | export type MapEntry = T extends Map ? [K, V] : never 5 | 6 | /** 7 | * @internal 8 | */ 9 | export type EitherOne = K extends keyof T 10 | ? { [P in K]: T[K] } & Partial<{ [P in Exclude]: never }> 11 | : never 12 | 13 | /** 14 | * @internal 15 | */ 16 | export function mapToSet( 17 | map: Map, 18 | { mapper, filter }: { mapper: (value: V, key: K) => R; filter?: (value: V, key: K) => boolean } 19 | ): Set { 20 | const set = new Set() 21 | 22 | map.forEach((value, key) => { 23 | if (!filter || (filter && filter(value, key))) { 24 | const item = mapper(value, key) 25 | set.add(item) 26 | } 27 | }) 28 | 29 | return set 30 | } 31 | 32 | /** 33 | * Manages creating and pushing to sub-arrays. 34 | * @internal 35 | */ 36 | export function pushDeep(arrayOfArrays: T[][], index: number, item: T) { 37 | if (arrayOfArrays[index]) { 38 | arrayOfArrays[index].push(item) 39 | } else { 40 | arrayOfArrays[index] = [item] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/path.ts: -------------------------------------------------------------------------------- 1 | import * as p from 'path' 2 | 3 | /** 4 | * @internal 5 | */ 6 | export function ensureAbsolutePath(path: string | undefined, basePath: string = process.cwd()): string { 7 | if (!path) return '' 8 | return p.isAbsolute(path) ? path : p.join(basePath, path) 9 | } 10 | 11 | /** 12 | * @internal 13 | */ 14 | export function relativeToCWD(path: string): string { 15 | return p.relative(process.cwd(), path) 16 | } 17 | 18 | /** 19 | * @internal 20 | */ 21 | export function parentPaths(path: string): string[] { 22 | // tslint:disable-next-line: prefer-const 23 | let { root, dir } = p.parse(path) 24 | const parents: string[] = [] 25 | 26 | while (dir !== root) { 27 | parents.push(dir) 28 | dir = p.dirname(dir) 29 | } 30 | 31 | return parents 32 | } 33 | 34 | /** 35 | * @internal 36 | */ 37 | export function fileIsWithin(file: string, dir: string) { 38 | const rel = p.relative(dir, file) 39 | return !rel.startsWith('../') && rel !== '..' 40 | } 41 | 42 | /** 43 | * @internal 44 | */ 45 | export function changeDir(file: string, fromDir: string, toDir: string): string { 46 | return p.resolve(toDir, p.relative(fromDir, file)) 47 | } 48 | 49 | /** 50 | * @param matchExtensions - extensions to replace, match everything if empty. Items should start with a dot. 51 | * @param newExtension - should start with a dot. 52 | * @internal 53 | */ 54 | export function changeExtension(file: string, matchExtensions: readonly string[], newExtension: string): string { 55 | const oldExtension = p.extname(file) 56 | 57 | if (matchExtensions.length === 0 || matchExtensions.includes(oldExtension)) { 58 | return p.join(p.dirname(file), p.basename(file, oldExtension) + newExtension) 59 | } 60 | 61 | return file 62 | } 63 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es2017", 5 | "module": "commonjs", 6 | "lib": ["es2017"], 7 | // "allowJs": true, 8 | // "checkJs": true, 9 | // "jsx": "preserve", 10 | "declaration": true, 11 | // "declarationMap": true, 12 | // "sourceMap": true, 13 | // "outFile": "./", 14 | "outDir": "./dist", 15 | // "rootDir": "./src", 16 | // "composite": true, 17 | // "removeComments": true, 18 | // "noEmit": true, 19 | // "importHelpers": true, 20 | // "downlevelIteration": true, 21 | // "isolatedModules": true, 22 | "newLine": "LF", 23 | 24 | /* Strict Type-Checking Options */ 25 | "strict": true, 26 | // "noImplicitAny": true, 27 | // "strictNullChecks": true, 28 | // "strictFunctionTypes": true, 29 | // "strictPropertyInitialization": true, 30 | // "noImplicitThis": true, 31 | // "alwaysStrict": true, 32 | 33 | /* Additional Checks */ 34 | // "noUnusedLocals": true, 35 | // "noUnusedParameters": true, 36 | // "noImplicitReturns": true, 37 | // "noFallthroughCasesInSwitch": true, 38 | 39 | /* Module Resolution Options */ 40 | "moduleResolution": "node", 41 | // "baseUrl": "./", 42 | // "paths": {}, 43 | // "rootDirs": [], 44 | // "typeRoots": ["node_modules/@types"], 45 | // "types": [], 46 | // "allowSyntheticDefaultImports": true, 47 | // "esModuleInterop": true, 48 | // "preserveSymlinks": true, 49 | "resolveJsonModule": true, 50 | 51 | /* Source Map Options */ 52 | // "sourceRoot": "", 53 | // "mapRoot": "", 54 | // "inlineSourceMap": true, 55 | // "inlineSources": true, 56 | 57 | /* Experimental Options */ 58 | // "experimentalDecorators": true, 59 | // "emitDecoratorMetadata": true, 60 | 61 | /* Compiler Plugins */ 62 | "plugins": [ 63 | { 64 | // https://github.com/Microsoft/typescript-tslint-plugin 65 | "name": "typescript-tslint-plugin", 66 | "configFile": "tslint.json" 67 | } 68 | ] 69 | }, 70 | "exclude": ["**/__tests__", "**/__fixtures__"] 71 | } 72 | --------------------------------------------------------------------------------