├── .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 |
--------------------------------------------------------------------------------