├── .codacy.yaml ├── .editorconfig ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── LICENSE ├── README.md ├── build └── build-dts.js ├── eslint.config.js ├── jest.config.js ├── package.json ├── prettier.config.js ├── rollup.config.js ├── src ├── api │ ├── emit-flat-dts.ts │ ├── flat-dts.ts │ └── index.ts ├── impl │ ├── create-flat-dts.ts │ ├── dts-content.ts │ ├── dts-mapper.ts │ ├── dts-meta.ts │ ├── dts-node-children.ts │ ├── dts-printer.ts │ ├── dts-setup.ts │ ├── dts-source-map.ts │ ├── dts-source.ts │ ├── dts-transformer.ts │ ├── index.ts │ ├── module-index.ts │ ├── module-info.ts │ ├── module-matcher.ts │ ├── simple-dts-printer.ts │ ├── source-map-dts-printer.ts │ └── transformed.ts ├── plugin.ts └── tests │ ├── classes │ ├── __snapshots__ │ │ └── classes.spec.ts.snap │ ├── classes.spec.ts │ ├── classes.ts │ └── tsconfig.json │ ├── constants │ ├── __snapshots__ │ │ └── constants.spec.ts.snap │ ├── constants.spec.ts │ ├── constants.ts │ └── tsconfig.json │ ├── emit-flat-dts.spec.ts │ ├── entries │ ├── __snapshots__ │ │ └── entries.spec.ts.snap │ ├── entries.spec.ts │ ├── entry1 │ │ └── index.ts │ ├── entry2 │ │ └── index.ts │ ├── root.ts │ └── tsconfig.json │ ├── functions │ ├── __snapshots__ │ │ └── functions.spec.ts.snap │ ├── functions.spec.ts │ ├── functions.ts │ └── tsconfig.json │ ├── imports │ ├── __snapshots__ │ │ └── imports.spec.ts.snap │ ├── imported.ts │ ├── importer.ts │ ├── imports.spec.ts │ └── tsconfig.json │ ├── interfaces │ ├── __snapshots__ │ │ └── interfaces.spec.ts.snap │ ├── interfaces.spec.ts │ ├── interfaces.ts │ └── tsconfig.json │ └── test-dts.ts ├── tsconfig.json ├── tsconfig.main.json ├── tsconfig.spec.json └── typedoc.json /.codacy.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | duplication: 4 | exclude_paths: 5 | - "src/**/*.spec.ts" 6 | - "src/spec/**" 7 | remark-lint: 8 | exclude_paths: 9 | - "**/*.md" 10 | 11 | exclude_paths: 12 | - "**/*.md" 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | max_line_length = 120 7 | indent_style = space 8 | indent_size = 2 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Build 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [master] 10 | pull_request: 11 | branches: [master] 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | # This workflow contains a single job called "build" 16 | build: 17 | # The type of runner that the job will run on 18 | runs-on: ubuntu-latest 19 | 20 | # Steps represent a sequence of tasks that will be executed as part of the job 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v3 24 | 25 | - name: Install Node.js 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: '18' 29 | 30 | - name: Install pnpm 31 | uses: pnpm/action-setup@v2 32 | id: pnpm-install 33 | with: 34 | version: '8' 35 | run_install: false 36 | 37 | - name: Get pnpm store directory 38 | id: pnpm-cache 39 | shell: bash 40 | run: | 41 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 42 | 43 | - name: Cache 44 | uses: actions/cache@v3 45 | env: 46 | cache-name: cache-node-modules-v3 47 | with: 48 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 49 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('package.json') }} 50 | 51 | - name: Install dependencies 52 | run: pnpm install 53 | 54 | - name: Build the project 55 | run: pnpm ci:all 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node.js modules 2 | node_modules 3 | 4 | # IntelliJ IDEA files 5 | .idea 6 | *.iml 7 | 8 | # Logs 9 | *.log 10 | 11 | # Yarn cache 12 | /.yarn 13 | !.yarn/releases 14 | !.yarn/plugins 15 | .pnp.* 16 | 17 | # Ignore lock files as this project developed within (private) pnpm worktree 18 | yarn.lock 19 | package-lock.json 20 | pnpm-lock.yaml 21 | 22 | # Package archive 23 | *.tgz 24 | 25 | # Intermediate files 26 | /target 27 | 28 | # Distribution directory 29 | /dist 30 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Node.js modules 2 | node_modules 3 | 4 | # IntelliJ IDEA files 5 | .idea 6 | *.iml 7 | 8 | # Intermediate files 9 | /target 10 | 11 | # Logs 12 | *.log 13 | 14 | # Package archive 15 | *.tgz 16 | 17 | # Source files 18 | /src 19 | 20 | # Build scripts 21 | /build 22 | 23 | # Build configurations 24 | /.* 25 | /*.cjs 26 | /*.js 27 | /*.json 28 | 29 | # Package lock 30 | /yarn.lock 31 | 32 | # Include distribution dir 33 | !/dist 34 | 35 | # Include type definitions 36 | !*.d.ts 37 | !*.d.ts.map 38 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /target 3 | /.idea 4 | __snapshots__ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2023 Ruslan Lopatin 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 | # Flatten `.d.ts` Files 2 | 3 | [![NPM][npm-image]][npm-url] 4 | [![Build Status][build-status-img]][build-status-link] 5 | [![Code Quality][quality-img]][quality-link] 6 | [![GitHub Project][github-image]][github-url] 7 | [![API Documentation][api-docs-image]][api documentation] 8 | 9 | [npm-image]: https://img.shields.io/npm/v/rollup-plugin-flat-dts.svg?logo=npm 10 | [npm-url]: https://www.npmjs.com/package/rollup-plugin-flat-dts 11 | [build-status-img]: https://github.com/run-z/rollup-plugin-flat-dts/workflows/Build/badge.svg 12 | [build-status-link]: https://github.com/run-z/rollup-plugin-flat-dts/actions?query=workflow:Build 13 | [quality-img]: https://app.codacy.com/project/badge/Grade/387d9672b1c546cda630bf3cb04d5cd2 14 | [quality-link]: https://app.codacy.com/gh/run-z/rollup-plugin-flat-dts/dashboard?utm_source=gh&utm_medium=referral&utm_content=run-z/rollup-plugin-flat-dts&utm_campaign=Badge_Grade 15 | [github-image]: https://img.shields.io/static/v1?logo=github&label=GitHub&message=project&color=informational 16 | [github-url]: https://github.com/run-z/rollup-plugin-flat-dts 17 | [api-docs-image]: https://img.shields.io/static/v1?logo=typescript&label=API&message=docs&color=informational 18 | [api documentation]: https://run-z.github.io/rollup-plugin-flat-dts 19 | 20 | ## Example Configuration 21 | 22 | Add the following `rollup.config.js`: 23 | 24 | ```javascript 25 | import commonjs from '@rollup/plugin-commonjs'; 26 | import nodeResolve from '@rollup/plugin-node-resolve'; 27 | import flatDts from 'rollup-plugin-flat-dts'; 28 | import sourcemaps from 'rollup-plugin-sourcemaps'; 29 | import ts from 'rollup-plugin-typescript2'; 30 | 31 | export default { 32 | input: './src/index.ts', 33 | plugins: [commonjs(), ts(), nodeResolve(), sourcemaps()], 34 | output: { 35 | format: 'esm', 36 | sourcemap: true, 37 | file: 'dist/index.js', 38 | plugins: [flatDts()], 39 | }, 40 | }; 41 | ``` 42 | 43 | Then the command `rollup --config ./rollup.config.js` would transpile TypeScript files from `src/` directory 44 | to `dist/index.js`, and generate `dist/index.d.ts` with flattened type definitions. 45 | 46 | ## Limitations 47 | 48 | This plugin flattens type definitions instead of merging them. This applies severe limitations to the source code: 49 | 50 | 1. Every export in every TypeScript file considered exported from the package (i.e. part of public API). 51 | 52 | Mark internal exports (internal API) with `@internal` jsdoc tag to prevent this, or declare internal modules with 53 | `internal` option. 54 | 55 | 2. Default exports supported only at the top level and in entry points ([see below](#multiple-entries)). 56 | 57 | 3. Exported symbols should be unique across the code base. 58 | 59 | 4. Exports should not be renamed when re-exporting them. 60 | 61 | Aliasing is still possible. 62 | 63 | ## Project Structure 64 | 65 | To adhere to these limitations the project structure could be like this: 66 | 67 | 1. An index file (`index.ts`) is present in each source directory with exported symbols. 68 | 69 | Index file re-exports all publicly available symbols from the same directory with statements like 70 | `export * from './file';`. 71 | 72 | Index file also re-export all symbols from nested directories with statements like `export * from './dir';` 73 | 74 | 2. All exported symbols that are not re-exported by index files considered part of internal API. 75 | 76 | Every such symbols has a jsdoc block containing `@internal` tag. 77 | 78 | Alternatively, the internal modules follow some naming convention. The `internal` option reflects this convention. 79 | E.g. `internal: ['**/impl/**', '**/*.impl']` would treat all `.impl.ts` source files and files in `impl/` directories 80 | as part of internal API. 81 | 82 | 3. Rollup entry points are index files. 83 | 84 | ## Configuration Options 85 | 86 | `flatDts({})` accepts configuration object with the following properties: 87 | 88 | - `tsconfig` - Either `tsconfig.json` file location relative to working directory, or parsed `tsconfig.json` contents. 89 | 90 | `tsconfig.json` by default. 91 | 92 | - `compilerOptions` - TypeScript compiler options to apply. 93 | 94 | Override the options from `tsconfig`. 95 | 96 | - `file` - Output `.d.ts` file name relative to output directory. 97 | 98 | `index.d.ts` by default. 99 | 100 | - `moduleName` - The module name to replace flattened module declarations with. 101 | 102 | Defaults to package name extracted from `package.json`. 103 | 104 | - `entries` - Module entries. 105 | 106 | A map of entry name declarations ([see below](#multiple-entries)). 107 | 108 | - `lib` - Whether to add [triple-slash] directives to refer the libraries used. 109 | 110 | Allowed values: 111 | 112 | - `true` to add an entry for each referred library from `lib` compiler option, 113 | - `false` (the default) to not add any library references, 114 | - an explicit list of libraries to refer. 115 | 116 | - `refs` - Whether to add file references. 117 | 118 | A file reference is added when one entry refers another one. 119 | 120 | `true` by default. 121 | 122 | - `external` - External module names. 123 | 124 | An array of external module names and their [glob] patterns. These names won't be changed during flattening 125 | process. 126 | 127 | This is useful for external module augmentation. 128 | 129 | - `internal` - Internal module names. 130 | 131 | An array of internal module names and their [glob] patterns. Internal module type definitions are excluded from 132 | generated `.d.ts` files. 133 | 134 | [triple-slash]: https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html 135 | [glob]: https://www.npmjs.com/package/micromatch 136 | 137 | ## Declaration Maps (Experimental) 138 | 139 | When [declarationMap] compiler option enabled a declaration map file(s) (`.d.ts.map`) will be generated next to `.d.ts` 140 | ones: 141 | 142 | ```javascript 143 | import commonjs from '@rollup/plugin-commonjs'; 144 | import nodeResolve from '@rollup/plugin-node-resolve'; 145 | import flatDts from 'rollup-plugin-flat-dts'; 146 | import sourcemaps from 'rollup-plugin-sourcemaps'; 147 | import ts from 'rollup-plugin-typescript2'; 148 | 149 | export default { 150 | input: './src/index.ts', 151 | plugins: [commonjs(), ts(), nodeResolve(), sourcemaps()], 152 | output: { 153 | format: 'esm', 154 | sourcemap: true, 155 | file: 'dist/index.js', 156 | plugins: [ 157 | flatDts({ 158 | compilerOptions: { 159 | declarationMap: true /* Generate declaration maps */, 160 | }, 161 | }), 162 | ], 163 | }, 164 | }; 165 | ``` 166 | 167 | Declaration maps can be used by IDE to navigate to TypeScript source file instead of type declaration when both 168 | available. This functionality relies on IDE heuristics, and declaration maps generated by this tool may not suit it 169 | fully. So, don't be surprised if that does not work as expected. 170 | 171 | [declarationmap]: https://www.typescriptlang.org/tsconfig#declarationMap 172 | 173 | ## Multiple Entries 174 | 175 | By default, the generated `.d.ts` file contains `declare module` statements with the same `moduleName`. 176 | 177 | If your package has additional [entry points] then you probably want to reflect this in type definition. 178 | This can be achieved with `entries` option. 179 | 180 | Here is an example configuration: 181 | 182 | ```javascript 183 | import commonjs from '@rollup/plugin-commonjs'; 184 | import nodeResolve from '@rollup/plugin-node-resolve'; 185 | import flatDts from 'rollup-plugin-flat-dts'; 186 | import sourcemaps from 'rollup-plugin-sourcemaps'; 187 | import ts from 'rollup-plugin-typescript2'; 188 | 189 | export default { 190 | input: { 191 | main: './src/index.ts', // Main entry point 192 | node: './src/node/index.ts', // Node.js-specific API 193 | web: './src/browser/index.ts', // Browser-specific API 194 | }, 195 | plugins: [commonjs(), ts(), nodeResolve(), sourcemaps()], 196 | output: { 197 | format: 'esm', 198 | sourcemap: true, 199 | dir: 'dist', // Place the output files to `dist` directory. 200 | entryFileNames: '[name].js', // Entry file names have `.js` extension. 201 | chunkFileNames: '_[name].js', // Chunks have underscore prefix. 202 | plugins: [ 203 | flatDts({ 204 | moduleName: 'my-package', // By default, exports belong to `my-package` module. 205 | entries: { 206 | node: {}, // All exports from `src/node` directory 207 | // belong to `my-package/node` sub-module. 208 | browser: { 209 | // All exports from `src/browser` directory 210 | as: 'web', // belong to `my-package/web` sub-module. 211 | // (Would be `my-package/browser` if omitted) 212 | lib: 'DOM', // Add `DOM` library reference. 213 | refs: false, // Do not add triple-slash file references to other entries. 214 | // Otherwise, a file reference will be added for each entry this one refers. 215 | file: 'web/index.d.ts', // Write type definitions to separate `.d.ts` file. 216 | // (Would be written to main `index.d.ts` if omitted) 217 | }, 218 | }, 219 | }), 220 | ], 221 | }, 222 | }; 223 | ``` 224 | 225 | The `package.json` would contain the following then: 226 | 227 | ```json 228 | { 229 | "name": "my-package", 230 | "type": "module", 231 | "types": "./dist/index.d.ts", 232 | "exports": { 233 | ".": "./dist/main.js", 234 | "./node": "./dist/node.js", 235 | "./web": "./dist/web.js" 236 | } 237 | } 238 | ``` 239 | 240 | [entry points]: https://nodejs.org/dist/latest-v14.x/docs/api/packages.html#packages_package_entry_points 241 | 242 | ## Standalone Usage 243 | 244 | The API is available standalone, without using Rollup: 245 | 246 | ```javascript 247 | import { emitFlatDts } from 'rollup-plugin-flat-dts/api'; 248 | 249 | emitFlatDts({ 250 | /* Type definition options */ 251 | }) 252 | .then(flatDts => { 253 | if (flatDts.diagnostics.length) { 254 | console.error(flatDts.formatDiagnostics()); 255 | } 256 | return flatDts.writeOut(); 257 | }) 258 | .catch(error => { 259 | console.error('Failed to generate type definitions', error); 260 | process.exit(1); 261 | }); 262 | ``` 263 | 264 | ## Algorithm Explained 265 | 266 | The plugin algorithm is not very sophisticated, but simple and straightforward. This made it actually usable on my 267 | projects, where [other tools] failed to generate valid type definitions. 268 | 269 | The simplicity comes at a price. So, this plugin applies limitations on code base rather trying to resolve all 270 | non-trivial cases. 271 | 272 | So, here what this plugin is doing: 273 | 274 | 1. Generates single-file type definition by overriding original `tsconfig.json` options with the following: 275 | 276 | ```jsonc 277 | { 278 | // Avoid extra work 279 | "checkJs": false, 280 | // Ensure ".d.ts" modules are generated 281 | "declaration": true, 282 | // Prevent output to declaration directory 283 | "declarationDir": null, 284 | // Skip ".js" generation 285 | "emitDeclarationOnly": true, 286 | // Single file emission is impossible with this flag set 287 | "isolatedModules": false, 288 | // Generate single file 289 | // `System`, in contrast to `None`, permits the use of `import.meta` 290 | "module": "System", 291 | // When set to "Node16" or "NodeNext", or when unspecified 292 | // Otherwise, it conflicts with SystemJS 293 | "moduleResolution": "Node", 294 | // Always emit 295 | "noEmit": false, 296 | // Skip code generation when error occurs 297 | "noEmitOnError": true, 298 | // SystemJS does not support JSON module imports 299 | "resolveJsonModule": false, 300 | // Ignore errors in library type definitions 301 | "skipLibCheck": true, 302 | // Always strip internal exports 303 | "stripInternal": true, 304 | // Unsupported by SystemJS 305 | "verbatimModuleSyntax": false, 306 | } 307 | ``` 308 | 309 | 2. The generated file consists of `declare module "path/to/file" { ... }` statements. One such statement per each 310 | source file. 311 | 312 | The plugin replaces all `"path/to/file"` references with corresponding module name. I.e. either with 313 | `${packageName}`, or `${packageName}/${entry.as}` for matching entry point. 314 | 315 | 3. Updates all `import` and `export` statements and adjusts module references. 316 | 317 | Some imports and exports removed along the way. E.g. there is no point to import to the module from itself, unless 318 | the named import or export assigns an alias to the imported symbol. 319 | 320 | 4. Updates inner module declarations. 321 | 322 | Just like `2.`, but also expands declarations if inner module receives the same name as enclosing one. 323 | 324 | 5. Removes module declarations that became (or was originally) empty. 325 | 326 | ## Other Tools 327 | 328 | [other tools]: #other-tools 329 | 330 | - [rollup-plugin-dts] Does not support multiple entries, as far as I know. 331 | - [@wessberg/rollup-plugin-ts] Is able to generate merged type definitions as well. 332 | - [API extractor] Does not support multiple entries intentionally. 333 | 334 | See more [here](https://github.com/timocov/dts-bundle-generator/discussions/68). 335 | 336 | [rollup-plugin-dts]: https://www.npmjs.com/package/rollup-plugin-dts 337 | [@wessberg/rollup-plugin-ts]: https://www.npmjs.com/package/@wessberg/rollup-plugin-ts 338 | [api extractor]: https://api-extractor.com/ 339 | -------------------------------------------------------------------------------- /build/build-dts.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { emitFlatDts } from 'rollup-plugin-flat-dts/api'; 4 | 5 | const moduleFile = fileURLToPath(import.meta.url); 6 | const moduleDir = path.dirname(moduleFile); 7 | const rootDir = path.normalize(path.join(moduleDir, '..')); 8 | 9 | emitFlatDts({ 10 | tsconfig: path.join(rootDir, 'tsconfig.json'), 11 | moduleName: 'rollup-plugin-flat-dts', 12 | lib: true, 13 | compilerOptions: { 14 | declarationMap: true, 15 | }, 16 | file: 'dist/flat-dts.plugin.d.ts', 17 | entries: { 18 | api: { file: 'dist/flat-dts.api.d.ts' }, 19 | }, 20 | internal: 'impl', 21 | }) 22 | .then(flatDts => { 23 | if (flatDts.diagnostics.length) { 24 | console.error(flatDts.formatDiagnostics()); 25 | } 26 | 27 | return flatDts.writeOut(); 28 | }) 29 | .catch(error => { 30 | console.error('Failed to generate type definitions', error); 31 | process.exit(1); 32 | }); 33 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import configs from '@run-z/eslint-config'; 2 | 3 | export default configs; 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | preset: 'ts-jest/presets/default-esm', 3 | collectCoverage: true, 4 | collectCoverageFrom: [ 5 | 'src/**/*.ts', 6 | '!src/tests/**', // Exclude tests 7 | '!src/**/index.ts', 8 | '!src/**/main.ts', 9 | '!**/node_modules/**', 10 | ], 11 | coverageDirectory: 'target/coverage', 12 | coverageThreshold: { 13 | global: {}, 14 | }, 15 | extensionsToTreatAsEsm: ['.ts'], 16 | reporters: [ 17 | 'default', 18 | [ 19 | 'jest-junit', 20 | { 21 | suiteName: 'rollup-plugin-flat-dts', 22 | outputDirectory: './target/test-results', 23 | classNameTemplate: '{classname}: {title}', 24 | titleTemplate: '{classname}: {title}', 25 | ancestorSeparator: ' › ', 26 | usePathForSuiteName: 'true', 27 | }, 28 | ], 29 | ], 30 | testEnvironment: 'node', 31 | transform: { 32 | '^.+\\.tsx?$': [ 33 | 'ts-jest', 34 | { 35 | tsconfig: 'tsconfig.spec.json', 36 | useESM: true, 37 | }, 38 | ], 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rollup-plugin-flat-dts", 3 | "version": "2.9.0", 4 | "description": ".d.ts files flattener and Rollup plugin", 5 | "keywords": [ 6 | "rollup-plugin", 7 | "typescript", 8 | "dts", 9 | "@types" 10 | ], 11 | "homepage": "https://github.com/run-z/rollup-plugin-flat-dts", 12 | "repository": { 13 | "type": "git", 14 | "url": "ssh://git@github.com:run-z/rollup-plugin-flat-dts.git" 15 | }, 16 | "license": "MIT", 17 | "author": "Ruslan Lopatin ", 18 | "bugs": { 19 | "url": "https://github.com/run-z/rollup-plugin-flat-dts/issues" 20 | }, 21 | "type": "module", 22 | "types": "./dist/flat-dts.plugin.d.ts", 23 | "typesVersions": { 24 | "*": { 25 | "api": [ 26 | "./dist/flat-dts.api.d.ts" 27 | ] 28 | } 29 | }, 30 | "exports": { 31 | ".": { 32 | "types": "./dist/flat-dts.plugin.d.ts", 33 | "require": "./dist/flat-dts.plugin.cjs", 34 | "default": "./dist/flat-dts.plugin.js" 35 | }, 36 | "./api": { 37 | "types": "./dist/flat-dts.api.d.ts", 38 | "require": "./dist/flat-dts.api.cjs", 39 | "default": "./dist/flat-dts.api.js" 40 | } 41 | }, 42 | "sideEffects": false, 43 | "peerDependencies": { 44 | "rollup": "2.79.1 - 5.0", 45 | "typescript": "4.8.4 - 5.8" 46 | }, 47 | "peerDependenciesMeta": { 48 | "rollup": { 49 | "optional": true 50 | } 51 | }, 52 | "dependencies": { 53 | "is-glob": "^4.0.3", 54 | "micromatch": "^4.0.8", 55 | "source-map": "^0.7.4" 56 | }, 57 | "devDependencies": { 58 | "@jest/globals": "^29.7.0", 59 | "@rollup/plugin-typescript": "^12.1.1", 60 | "@run-z/eslint-config": "^5.0.0", 61 | "@run-z/prettier-config": "^3.0.0", 62 | "@types/is-glob": "^4.0.4", 63 | "@types/micromatch": "^4.0.9", 64 | "@types/node": "^22.10.0", 65 | "eslint": "^9.15.0", 66 | "gh-pages": "^6.2.0", 67 | "jest": "^29.7.0", 68 | "jest-junit": "^16.0.0", 69 | "prettier": "^3.3.3", 70 | "rollup": "^4.27.4", 71 | "run-z": "2.1.0-bootstrap", 72 | "shx": "^0.3.4", 73 | "ts-jest": "^29.2.5", 74 | "tslib": "^2.8.1", 75 | "typedoc": "^0.26.11", 76 | "typescript": "~5.7.2" 77 | }, 78 | "scripts": { 79 | "all": "run-z +z build,lint,test", 80 | "bootstrap": "rollup -c && node --enable-source-maps ./build/build-dts.js", 81 | "build": "run-z +z build:bundle build:dts", 82 | "build:bundle": "run-z +z --then rollup -c", 83 | "build:dts": "run-z +z --then node --enable-source-maps ./build/build-dts.js", 84 | "ci:all": "run-z all +cmd:jest/--ci/--runInBand", 85 | "clean": "run-z +z --then shx rm -rf dist target", 86 | "doc": "run-z +z --then typedoc", 87 | "doc:publish": "run-z doc --then gh-pages --dist target/typedoc --dotfiles", 88 | "dts": "node --enable-source-maps ./build/build-dts.js", 89 | "format": "run-z +z --then prettier --write \"src/**/*.*\" \"*.{js,cjs,json,md}\"", 90 | "lint": "run-z +z --then eslint .", 91 | "test": "run-z +z env:NODE_OPTIONS=\"--experimental-vm-modules --no-warnings\" --then jest", 92 | "z": "run-z +cmd:rollup,+cmd:eslint,+cmd:jest +dts,+cmd:eslint,+cmd:jest" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | import config from '@run-z/prettier-config'; 2 | 3 | export default config; 4 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import ts from '@rollup/plugin-typescript'; 2 | import { builtinModules, createRequire } from 'node:module'; 3 | import path from 'node:path'; 4 | import { defineConfig } from 'rollup'; 5 | import typescript from 'typescript'; 6 | 7 | const req = createRequire(import.meta.url); 8 | const pkg = req('./package.json'); 9 | const externals = new Set([ 10 | ...builtinModules, 11 | ...Object.keys(pkg.peerDependencies), 12 | ...Object.keys(pkg.dependencies), 13 | ]); 14 | 15 | export default defineConfig({ 16 | input: { 17 | 'flat-dts.api': './src/api/index.ts', 18 | 'flat-dts.plugin': './src/plugin.ts', 19 | }, 20 | plugins: [ 21 | ts({ 22 | typescript, 23 | cacheDir: 'target/.rts_cache', 24 | }), 25 | ], 26 | external(id) { 27 | return id.startsWith('node:') || externals.has(id); 28 | }, 29 | output: [ 30 | { 31 | format: 'cjs', 32 | sourcemap: true, 33 | dir: '.', 34 | exports: 'auto', 35 | entryFileNames: 'dist/[name].cjs', 36 | chunkFileNames: 'dist/_[name].cjs', 37 | manualChunks, 38 | hoistTransitiveImports: false, 39 | }, 40 | { 41 | format: 'esm', 42 | sourcemap: true, 43 | dir: '.', 44 | entryFileNames: 'dist/[name].js', 45 | chunkFileNames: 'dist/_[name].js', 46 | manualChunks, 47 | hoistTransitiveImports: false, 48 | }, 49 | ], 50 | }); 51 | 52 | function manualChunks(id) { 53 | if (id === path.resolve('src', 'plugin.ts')) { 54 | return 'flat-dts.plugin'; 55 | } 56 | if (id.startsWith(path.resolve('src'))) { 57 | return 'flat-dts.api'; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/api/emit-flat-dts.ts: -------------------------------------------------------------------------------- 1 | import ts from 'typescript'; 2 | import { DtsSetup, DtsSource, DtsSourceFile, DtsTransformer, emptyFlatDts } from '../impl'; 3 | import type { FlatDts } from './flat-dts'; 4 | 5 | /** 6 | * Emits flat type definitions. 7 | * 8 | * Does not actually writes to `.d.ts` files. 9 | * 10 | * @param dtsOptions - Flattening options. 11 | * 12 | * @returns A promise resolved to flattened type definitions representation. 13 | */ 14 | export async function emitFlatDts(dtsOptions: FlatDts.Options = {}): Promise { 15 | const setup = new DtsSetup(dtsOptions); 16 | const { compilerOptions, files, errors } = setup; 17 | 18 | const program = ts.createProgram({ 19 | rootNames: files, 20 | options: compilerOptions, 21 | host: ts.createCompilerHost(compilerOptions, true), 22 | configFileParsingDiagnostics: errors, 23 | }); 24 | 25 | const { sources, diagnostics } = await new Promise(resolve => { 26 | const sources: DtsSourceFile[] = []; 27 | 28 | try { 29 | const { diagnostics } = program.emit( 30 | undefined /* all source files */, 31 | (path, content) => { 32 | sources.push({ path, content }); 33 | }, 34 | undefined /* cancellationToken */, 35 | true /* emitOnlyDtsFiles */, 36 | ); 37 | 38 | resolve({ sources, diagnostics }); 39 | } catch (error) { 40 | resolve({ 41 | sources, 42 | diagnostics: [ 43 | { 44 | category: ts.DiagnosticCategory.Error, 45 | code: 9999, 46 | file: undefined, 47 | start: undefined, 48 | length: undefined, 49 | messageText: error instanceof Error ? error.message : String(error), 50 | }, 51 | ], 52 | }); 53 | } 54 | }); 55 | 56 | const source = await DtsSource.create(sources, setup); 57 | 58 | if (!source) { 59 | return emptyFlatDts(diagnostics); 60 | } 61 | 62 | try { 63 | const transformer = new DtsTransformer(source); 64 | 65 | return transformer.transform(diagnostics); 66 | } finally { 67 | source.destroy(); 68 | } 69 | } 70 | 71 | interface EmittedDts { 72 | readonly sources: readonly DtsSourceFile[]; 73 | 74 | readonly diagnostics: readonly ts.Diagnostic[]; 75 | } 76 | -------------------------------------------------------------------------------- /src/api/flat-dts.ts: -------------------------------------------------------------------------------- 1 | import type ts from 'typescript'; 2 | 3 | /** 4 | * Flattened type definitions. 5 | */ 6 | export interface FlatDts { 7 | /** 8 | * An array of emitted `.d.ts` files. 9 | * 10 | * May be empty when emission failed. 11 | */ 12 | readonly files: readonly FlatDts.File[]; 13 | 14 | /** 15 | * Emission diagnostics. 16 | */ 17 | readonly diagnostics: readonly ts.Diagnostic[]; 18 | 19 | /** 20 | * Formats emission diagnostics. 21 | * 22 | * @returns A string containing formatted diagnostic messages. 23 | */ 24 | formatDiagnostics(): string; 25 | 26 | /** 27 | * Writes all type definition files. 28 | * 29 | * @param rootDir - A root directory to place `.d.ts` files to. Defaults to working directory. 30 | * 31 | * @returns A promise resolved when all files written. 32 | */ 33 | writeOut(rootDir: string): Promise; 34 | } 35 | 36 | export namespace FlatDts { 37 | /** 38 | * Type definitions flattening options. 39 | * 40 | * Contains options for `rollup-plugin-flat-dts` plugin. 41 | * 42 | * Accepted by {@link rollup-plugin-flat-dts/api!emitFlatDts emitFlatDts} function. 43 | */ 44 | export interface Options { 45 | /** 46 | * Either `tsconfig.json` file location relative to working directory, or parsed `tsconfig.json` contents. 47 | * 48 | * @defaultValue `"tsconfig.json"` 49 | */ 50 | readonly tsconfig?: string | unknown | undefined; 51 | 52 | /** 53 | * TypeScript compiler options to apply. 54 | * 55 | * Override the options from {@link tsconfig}. 56 | */ 57 | readonly compilerOptions?: ts.CompilerOptions | undefined; 58 | 59 | /** 60 | * Output `.d.ts` file name relative to output directory. 61 | * 62 | * @defaultValue `index.d.ts` 63 | */ 64 | readonly file?: string | undefined; 65 | 66 | /** 67 | * The module name to replace flattened module declarations with. 68 | * 69 | * @defaultValue Package name extracted from `package.json` found in current directory. 70 | */ 71 | readonly moduleName?: string | undefined; 72 | 73 | /** 74 | * Module entries. 75 | * 76 | * A map of entry name declarations. Each key is an original name of module entry as it present in non-flattened 77 | * `.d.ts` file, which is typically a relative path to original typescript file without `.ts` extension. 78 | */ 79 | readonly entries?: { 80 | readonly [name: string]: EntryDecl | undefined; 81 | }; 82 | 83 | /** 84 | * Whether to add [triple-slash](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html) 85 | * directives to refer the libraries used. 86 | * 87 | * Allowed values: 88 | * - `true` to add an entry for each referred library from `lib` compiler option, 89 | * - `false` (the default) to not add any library references, 90 | * - an explicit list of libraries to refer. 91 | * 92 | * @defaultValue `false` 93 | */ 94 | readonly lib?: boolean | string | readonly string[] | undefined; 95 | 96 | /** 97 | * Whether to add file references. 98 | * 99 | * A file reference is added when one entry refers another one. 100 | * 101 | * @defaultValue `true` 102 | */ 103 | readonly refs?: boolean | undefined; 104 | 105 | /** 106 | * External module names. 107 | * 108 | * An array of external module names and their [glob] patterns. These names won't be changed during flattening 109 | * process. 110 | * 111 | * This is useful for external module augmentation. 112 | * 113 | * [glob]: https://www.npmjs.com/package/micromatch 114 | */ 115 | readonly external?: string | readonly string[] | undefined; 116 | 117 | /** 118 | * Internal module names. 119 | * 120 | * An array of internal module names and their [glob] patterns. Internal module type definitions are excluded from 121 | * generated `.d.ts` files. 122 | * 123 | * [glob]: https://www.npmjs.com/package/micromatch 124 | */ 125 | readonly internal?: string | readonly string[] | undefined; 126 | } 127 | 128 | /** 129 | * Declaration of module entry. 130 | */ 131 | export interface EntryDecl { 132 | /** 133 | * Final entry name. 134 | * 135 | * When specified, the original entry name is replaced with `/`. 136 | * 137 | * @defaultValue The same as entry name. 138 | */ 139 | readonly as?: string | undefined; 140 | 141 | /** 142 | * Whether to add [triple-slash](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html) 143 | * directives to refer the libraries used by this entry. 144 | * 145 | * Allowed values: 146 | * - `true` to add an entry for each referred library from `lib` compiler option, 147 | * - `false` (the default) to not add any library references, 148 | * - an explicit list of libraries to refer. 149 | * 150 | * @defaultValue Inherited from {@link Options.lib `lib` flattening option}. 151 | */ 152 | readonly lib?: boolean | string | readonly string[] | undefined; 153 | 154 | /** 155 | * Output `.d.ts` file name relative to output directory. 156 | * 157 | * When omitted the contents are merged into main `.d.ts.` file. 158 | */ 159 | readonly file?: string | undefined; 160 | 161 | /** 162 | * Whether to add file references. 163 | * 164 | * A file reference is added for each entry this one refers. 165 | * 166 | * @defaultValue Inherited from {@link Options.refs `refs` flattening option}. 167 | */ 168 | readonly refs?: boolean | undefined; 169 | } 170 | 171 | /** 172 | * Emitted `.d.ts` file. 173 | * 174 | * The file is not actually written to the disk by {@link File.writeOut} call. 175 | */ 176 | export interface File { 177 | /** 178 | * Emitted `.d.ts` file path. 179 | */ 180 | readonly path: string; 181 | 182 | /** 183 | * Emitted `.d.ts` file contents. 184 | */ 185 | readonly content: string; 186 | 187 | /** 188 | * Writes contents to this file. 189 | * 190 | * Creates the necessary directories. 191 | * 192 | * @param path - Target file path. Defaults to {@link path}. 193 | * 194 | * @returns A promise resolved when file written. 195 | */ 196 | writeOut(path?: string): Promise; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module rollup-plugin-flat-dts/api 3 | */ 4 | export * from './emit-flat-dts'; 5 | export * from './flat-dts'; 6 | -------------------------------------------------------------------------------- /src/impl/create-flat-dts.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path'; 2 | import ts from 'typescript'; 3 | import type { FlatDts } from '../api'; 4 | 5 | const FORMAT_HOST: ts.FormatDiagnosticsHost = { 6 | getCurrentDirectory: () => ts.sys.getCurrentDirectory(), 7 | getNewLine: () => ts.sys.newLine, 8 | getCanonicalFileName: ts.sys.useCaseSensitiveFileNames ? f => f : f => f.toLowerCase(), 9 | }; 10 | 11 | function formatDiagnostics(this: FlatDts): string { 12 | return this.diagnostics.length 13 | ? ts.formatDiagnosticsWithColorAndContext(this.diagnostics, FORMAT_HOST) 14 | : ''; 15 | } 16 | 17 | export function emptyFlatDts(diagnostics: readonly ts.Diagnostic[]): FlatDts { 18 | return { 19 | files: [], 20 | diagnostics, 21 | formatDiagnostics, 22 | writeOut() { 23 | return Promise.reject(new Error('Failed to emit type definitions')); 24 | }, 25 | }; 26 | } 27 | 28 | export function createFlatDts( 29 | files: readonly FlatDts.File[], 30 | diagnostics: readonly ts.Diagnostic[] = [], 31 | ): FlatDts { 32 | return { 33 | files, 34 | diagnostics, 35 | formatDiagnostics, 36 | writeOut(rootDir) { 37 | const filePath: (file: FlatDts.File) => string | undefined = 38 | rootDir != null ? ({ path }) => resolve(rootDir, path) : ({ path }) => path; 39 | 40 | return Promise.all(files.map(file => file.writeOut(filePath(file)))).then(() => void 0); 41 | }, 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /src/impl/dts-content.ts: -------------------------------------------------------------------------------- 1 | import type ts from 'typescript'; 2 | import type { FlatDts } from '../api'; 3 | import type { DtsPrinter } from './dts-printer'; 4 | import type { DtsSource } from './dts-source'; 5 | import type { ModuleInfo } from './module-info'; 6 | import { SimpleDtsPrinter } from './simple-dts-printer'; 7 | import { SourceMapDtsPrinter } from './source-map-dts-printer'; 8 | 9 | export class DtsContent { 10 | private readonly _refs = new Set(); 11 | private readonly _statements: ts.Statement[] = []; 12 | 13 | constructor( 14 | readonly source: DtsSource, 15 | readonly module: ModuleInfo, 16 | ) {} 17 | 18 | refer(refs: readonly ModuleInfo[] | undefined): this { 19 | if (refs) { 20 | refs.forEach(ref => this._refs.add(ref)); 21 | } 22 | 23 | return this; 24 | } 25 | 26 | append(statement: ts.Statement): void { 27 | this._statements.push(statement); 28 | } 29 | 30 | toFiles(): readonly FlatDts.File[] { 31 | const printer = this.source.hasMap() 32 | ? new SourceMapDtsPrinter(this.source) 33 | : new SimpleDtsPrinter(this.source); 34 | 35 | this.module.prelude(printer); 36 | this._prelude(printer); 37 | 38 | this._statements.forEach((statement, i) => { 39 | if (i) { 40 | printer.nl(); 41 | } 42 | printer.print(statement).nl(); 43 | }); 44 | 45 | return printer.toFiles(this.module.file!); 46 | } 47 | 48 | private _prelude(printer: DtsPrinter): void { 49 | if (this.module.refs) { 50 | for (const ref of this._refs) { 51 | const path = this.module.pathTo(ref); 52 | 53 | if (path) { 54 | // No need to refer itself. 55 | printer.text(`/// `).nl(); 56 | } 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/impl/dts-mapper.ts: -------------------------------------------------------------------------------- 1 | import { Mapping, SourceMapGenerator } from 'source-map'; 2 | import ts from 'typescript'; 3 | import type { FlatDts } from '../api'; 4 | import { DtsNodeChildren } from './dts-node-children'; 5 | import type { DtsSource } from './dts-source'; 6 | 7 | export class DtsMapper { 8 | private readonly _genDts: ts.SourceFile; 9 | private readonly _generator: SourceMapGenerator; 10 | private readonly _line: Mapping[] = []; 11 | 12 | constructor( 13 | private readonly _source: DtsSource.WithMap, 14 | dtsFile: FlatDts.File, 15 | ) { 16 | const { setup } = _source; 17 | 18 | // Re-parse just generated `.d.ts`. 19 | this._genDts = ts.createSourceFile( 20 | dtsFile.path, 21 | dtsFile.content, 22 | setup.scriptTarget, 23 | false, 24 | ts.ScriptKind.TS, 25 | ); 26 | 27 | this._generator = new SourceMapGenerator({ 28 | sourceRoot: setup.pathToRoot(dtsFile.path), 29 | file: setup.basename(dtsFile.path), 30 | }); 31 | } 32 | 33 | map(orgNodes: readonly ts.Node[]): string { 34 | this._mapNodes(orgNodes, this._genDts.statements); 35 | this._endLine(); 36 | 37 | return this._generator.toString(); 38 | } 39 | 40 | private _mapNodes(orgNodes: Iterable, genNodes: Iterable): void { 41 | // Assume the re-parsed AST has the same structure as an original one. 42 | 43 | const orgIt = orgNodes[Symbol.iterator](); 44 | const genIt = genNodes[Symbol.iterator](); 45 | 46 | for (;;) { 47 | const orgNext = orgIt.next(); 48 | const genNext = genIt.next(); 49 | 50 | if (orgNext.done || genNext.done) { 51 | break; 52 | } 53 | 54 | this._mapNode(orgNext.value, genNext.value); 55 | } 56 | } 57 | 58 | private _mapNode(orgNode: ts.Node, genNode: ts.Node): void { 59 | const orgRange = this._source.map.originalRange(orgNode, this._source.source); 60 | const genStartPos = genNode.getStart(this._genDts); 61 | 62 | if (!orgRange || genStartPos < 0) { 63 | this._mapChildren(orgNode, genNode); 64 | 65 | return; 66 | } 67 | 68 | const [orgStart, orgEnd] = orgRange; 69 | const genStart = this._genDts.getLineAndCharacterOfPosition(genStartPos); 70 | 71 | this._addMapping({ 72 | generated: { line: genStart.line + 1, column: genStart.character }, 73 | original: { line: orgStart.line + 1, column: orgStart.col }, 74 | source: orgStart.source, 75 | }); 76 | 77 | this._mapChildren(orgNode, genNode); 78 | 79 | const genEnd = this._genDts.getLineAndCharacterOfPosition(genNode.getEnd()); 80 | 81 | this._addMapping({ 82 | generated: { line: genEnd.line + 1, column: genEnd.character }, 83 | original: { line: orgEnd.line + 1, column: orgEnd.col }, 84 | source: orgEnd.source, 85 | }); 86 | } 87 | 88 | private _mapChildren(orgNode: ts.Node, genNode: ts.Node): void { 89 | this._mapNodes(new DtsNodeChildren(orgNode), new DtsNodeChildren(genNode)); 90 | } 91 | 92 | private _addMapping(mapping: Mapping): void { 93 | const [prev] = this._line; 94 | 95 | if ( 96 | prev && 97 | prev.source === mapping.source && 98 | prev.generated.line === mapping.generated.line && 99 | prev.original.line === mapping.original.line 100 | ) { 101 | // Mapping from and to the same line 102 | this._line.push(mapping); 103 | 104 | return; 105 | } 106 | 107 | this._endLine(); 108 | this._line.length = 0; 109 | this._line.push(mapping); 110 | } 111 | 112 | _endLine(): void { 113 | // Sort line mappings by column number 114 | this._line.sort(compareMappingColumns); 115 | 116 | const lastIdx = this._line.length - 1; 117 | 118 | this._line.forEach((mapping, i) => { 119 | if (i && i < lastIdx) { 120 | // Always record the first and the last mapping 121 | 122 | const prev = this._line[i - 1]; 123 | const genOffset = mapping.generated.column - prev.generated.column; 124 | 125 | if (!genOffset) { 126 | // No need to record the same mapping twice. 127 | return; 128 | } 129 | /* 130 | // Disabled. It seems that spanning subsequent segments breaks IDE navigation. 131 | 132 | const orgOffset = mapping.original.column - prev.original.column; 133 | 134 | if (genOffset === orgOffset) { 135 | // The column offset remained the same. 136 | // Span with the previous mapping segment. 137 | return; 138 | } 139 | */ 140 | } 141 | 142 | this._generator.addMapping(mapping); 143 | }); 144 | } 145 | } 146 | 147 | function compareMappingColumns( 148 | { generated: { column: col1 } }: Mapping, 149 | { generated: { column: col2 } }: Mapping, 150 | ): number { 151 | return col1 - col2; 152 | } 153 | -------------------------------------------------------------------------------- /src/impl/dts-meta.ts: -------------------------------------------------------------------------------- 1 | import ts from 'typescript'; 2 | 3 | export class DtsMeta { 4 | private readonly _declaredModules: ReadonlySet; 5 | 6 | constructor(source: ts.SourceFile) { 7 | const declaredModules = new Set(); 8 | 9 | for (const statement of source.statements) { 10 | if (statement.kind === ts.SyntaxKind.ModuleDeclaration) { 11 | const { name } = statement as ts.ModuleDeclaration; 12 | 13 | if (!ts.isMemberName(name)) { 14 | declaredModules.add(name.text); 15 | } 16 | } 17 | } 18 | 19 | this._declaredModules = declaredModules; 20 | } 21 | 22 | isModuleDeclared(name: string): boolean { 23 | return this._declaredModules.has(name); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/impl/dts-node-children.ts: -------------------------------------------------------------------------------- 1 | import ts from 'typescript'; 2 | 3 | export class DtsNodeChildren extends Set { 4 | constructor(node: ts.Node) { 5 | super(); 6 | 7 | ts.forEachChild(node, child => { 8 | this.add(child); 9 | }); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/impl/dts-printer.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'node:fs'; 2 | import { dirname } from 'node:path'; 3 | import ts from 'typescript'; 4 | import type { FlatDts } from '../api'; 5 | import type { DtsSource } from './dts-source'; 6 | 7 | export abstract class DtsPrinter { 8 | private readonly _printer: ts.Printer; 9 | private _out = ''; 10 | 11 | constructor(readonly source: TSource) { 12 | this._printer = ts.createPrinter({ 13 | newLine: source.setup.compilerOptions.newLine, 14 | }); 15 | } 16 | 17 | print(node: ts.Node): this { 18 | this.text(this._printer.printNode(ts.EmitHint.Unspecified, node, this.source.source)); 19 | 20 | return this; 21 | } 22 | 23 | text(text: string): this { 24 | this._out += text; 25 | 26 | return this; 27 | } 28 | 29 | nl(): this { 30 | return this.text(this.source.setup.eol); 31 | } 32 | 33 | abstract toFiles(name: string): readonly FlatDts.File[]; 34 | 35 | protected createFile(path: string, content = this._out): FlatDts.File { 36 | return { 37 | path, 38 | content, 39 | async writeOut(filePath = path) { 40 | await fs.mkdir(dirname(filePath), { recursive: true }); 41 | 42 | return fs.writeFile(filePath, content); 43 | }, 44 | }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/impl/dts-setup.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import process from 'node:process'; 3 | import { pathToFileURL, URL } from 'node:url'; 4 | import ts, { type Diagnostic } from 'typescript'; 5 | import type { FlatDts } from '../api'; 6 | 7 | export class DtsSetup { 8 | readonly compilerOptions: Readonly; 9 | readonly files: readonly string[]; 10 | readonly errors: readonly ts.Diagnostic[]; 11 | readonly scriptTarget: ts.ScriptTarget; 12 | readonly eol: string; 13 | 14 | constructor(readonly dtsOptions: FlatDts.Options) { 15 | const { compilerOptions, files, errors } = parseDtsOptions(dtsOptions); 16 | 17 | this.compilerOptions = compilerOptions; 18 | this.files = files; 19 | this.errors = errors; 20 | 21 | this.scriptTarget = detectScriptTarget(compilerOptions); 22 | this.eol = detectEOL(compilerOptions); 23 | } 24 | 25 | sourceURL(path: string): URL { 26 | return new URL(path, this.root()); 27 | } 28 | 29 | root(): URL { 30 | return pathToFileURL('./'); 31 | } 32 | 33 | relativePath(path: string): string { 34 | const cwd = this.root(); 35 | const { href: cwdHref } = cwd; 36 | const { href } = new URL(path, cwd); 37 | 38 | if (!href.startsWith(cwdHref)) { 39 | return path; 40 | } 41 | 42 | return href.slice(cwdHref.length); 43 | } 44 | 45 | basename(path: string): string { 46 | const idx = path.lastIndexOf('/'); 47 | 48 | return idx < 0 ? path : path.slice(idx + 1); 49 | } 50 | 51 | pathToRoot(path: string): string { 52 | const cwd = this.root(); 53 | const { href: cwdHref } = cwd; 54 | const { href } = new URL(path, cwd); 55 | 56 | if (!href.startsWith(cwdHref)) { 57 | return path; 58 | } 59 | 60 | let relative = href.slice(cwdHref.length); 61 | let result = ''; 62 | 63 | for (;;) { 64 | const idx = relative.lastIndexOf('/'); 65 | 66 | if (idx < 0) { 67 | break; 68 | } 69 | 70 | if (result) { 71 | result += '/..'; 72 | } else { 73 | result = '..'; 74 | } 75 | 76 | relative = relative.slice(0, idx); 77 | } 78 | 79 | return result; 80 | } 81 | } 82 | 83 | function parseDtsOptions(dtsOptions: FlatDts.Options): { 84 | readonly compilerOptions: Readonly; 85 | readonly files: readonly string[]; 86 | readonly errors: readonly ts.Diagnostic[]; 87 | } { 88 | const { tsconfig = 'tsconfig.json', file: outFile = 'index.d.ts' } = dtsOptions; 89 | let { compilerOptions = {} } = dtsOptions; 90 | 91 | compilerOptions = { 92 | ...compilerOptions, 93 | ...MANDATORY_COMPILER_OPTIONS, 94 | outDir: undefined, 95 | outFile, 96 | }; 97 | 98 | let dirName: string; 99 | let tsconfigFile: string | undefined; 100 | let tsconfigJson: unknown; 101 | 102 | if (typeof tsconfig !== 'string') { 103 | dirName = process.cwd(); 104 | tsconfigJson = tsconfig; 105 | } else { 106 | dirName = path.dirname(tsconfig); 107 | tsconfigFile = path.basename(tsconfig); 108 | 109 | const configPath = ts.findConfigFile(dirName, ts.sys.fileExists, tsconfig); 110 | 111 | if (!configPath) { 112 | return { 113 | compilerOptions: patchCompilerOptions(compilerOptions), 114 | files: [], 115 | errors: [], 116 | }; 117 | } 118 | 119 | dirName = path.dirname(configPath); 120 | 121 | const { 122 | config, 123 | error, 124 | }: { 125 | readonly config?: unknown; 126 | readonly error?: Diagnostic; 127 | } = ts.readConfigFile(configPath, ts.sys.readFile); 128 | 129 | if (error) { 130 | return { 131 | compilerOptions: patchCompilerOptions(compilerOptions), 132 | files: [], 133 | errors: [error], 134 | }; 135 | } 136 | 137 | tsconfigJson = config; 138 | } 139 | 140 | const { 141 | options, 142 | errors, 143 | fileNames: files, 144 | } = ts.parseJsonConfigFileContent(tsconfigJson, ts.sys, dirName, undefined, tsconfigFile); 145 | 146 | return { 147 | compilerOptions: patchCompilerOptions({ 148 | ...options, 149 | ...compilerOptions, 150 | }), 151 | files, 152 | errors, 153 | }; 154 | } 155 | 156 | function patchCompilerOptions(compilerOptions: ts.CompilerOptions): ts.CompilerOptions { 157 | const { moduleResolution } = compilerOptions; 158 | 159 | if ( 160 | moduleResolution == null || 161 | moduleResolution === ts.ModuleResolutionKind.Node16 || 162 | moduleResolution === ts.ModuleResolutionKind.NodeNext 163 | ) { 164 | // SystemJS does not support `Node16` and `NodeNext` resolutions 165 | compilerOptions = { 166 | ...compilerOptions, 167 | moduleResolution: ts.ModuleResolutionKind.Node10, 168 | }; 169 | } 170 | 171 | return compilerOptions; 172 | } 173 | 174 | const MANDATORY_COMPILER_OPTIONS: ts.CompilerOptions = { 175 | // Avoid extra work 176 | checkJs: false, 177 | // Ensure ".d.ts" modules are generated 178 | declaration: true, 179 | // Prevent output to declaration directory 180 | declarationDir: undefined!, 181 | // Skip ".js" generation 182 | emitDeclarationOnly: true, 183 | // Single file emission is impossible with this flag set 184 | isolatedModules: false, 185 | // Generate single file 186 | // `System`, in contrast to `None`, permits the use of `import.meta` 187 | module: ts.ModuleKind.System, 188 | // Always emit 189 | noEmit: false, 190 | // Skip code generation when error occurs 191 | noEmitOnError: true, 192 | // SystemJS does not support JSON module imports 193 | resolveJsonModule: false, 194 | // Ignore errors in library type definitions 195 | skipLibCheck: true, 196 | // Always strip internal exports 197 | stripInternal: true, 198 | // Unsupported by SystemJS 199 | verbatimModuleSyntax: false, 200 | }; 201 | 202 | function detectScriptTarget(compilerOptions: ts.CompilerOptions): ts.ScriptTarget { 203 | let { target } = compilerOptions; 204 | 205 | if (!target) { 206 | // Set target to latest if absent 207 | compilerOptions.target = target = ts.ScriptTarget.Latest; 208 | } 209 | 210 | return target; 211 | } 212 | 213 | function detectEOL({ newLine }: ts.CompilerOptions): string { 214 | switch (newLine) { 215 | case ts.NewLineKind.LineFeed: 216 | return '\n'; 217 | case ts.NewLineKind.CarriageReturnLineFeed: 218 | return '\r\n'; 219 | default: 220 | return ts.sys.newLine; 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/impl/dts-source-map.ts: -------------------------------------------------------------------------------- 1 | import { SourceMapConsumer } from 'source-map'; 2 | import type ts from 'typescript'; 3 | import type { DtsSetup } from './dts-setup'; 4 | 5 | export class DtsSourceMap { 6 | static async create(path: string, content: string, setup: DtsSetup): Promise { 7 | return new DtsSourceMap( 8 | await new SourceMapConsumer(content, setup.sourceURL(path).href), 9 | setup, 10 | ); 11 | } 12 | 13 | private constructor( 14 | readonly map: SourceMapConsumer, 15 | readonly setup: DtsSetup, 16 | ) {} 17 | 18 | originalRange(node: ts.Node, source: ts.SourceFile): DtsLocationRange | undefined { 19 | if (!(node.pos >= 0) || !(node.end >= 0)) { 20 | return; 21 | } 22 | 23 | const startPos = node.getStart(source); 24 | const endPos = node.getEnd(); 25 | 26 | if (startPos < 0 || endPos < 0) { 27 | return; 28 | } 29 | 30 | const srcStart = this._sourceLocation(source, startPos); 31 | 32 | if (!srcStart) { 33 | return; 34 | } 35 | 36 | const srcEnd = this._sourceLocation(source, endPos); 37 | 38 | if (!srcEnd) { 39 | return; 40 | } 41 | 42 | return [srcStart, srcEnd]; 43 | } 44 | 45 | destroy(): void { 46 | this.map.destroy(); 47 | } 48 | 49 | private _sourceLocation(sourceFile: ts.SourceFile, pos: number): DtsLocation | undefined { 50 | if (pos < 0) { 51 | return; 52 | } 53 | 54 | const location = sourceFile.getLineAndCharacterOfPosition(pos); 55 | const { source, line, column } = this.map.originalPositionFor({ 56 | line: location.line + 1, 57 | column: location.character, 58 | }); 59 | 60 | if (source == null || line == null || column == null) { 61 | return; 62 | } 63 | 64 | return { 65 | source: this.setup.relativePath(source), 66 | line: line - 1, 67 | col: column, 68 | }; 69 | } 70 | } 71 | 72 | export type DtsLocationRange = readonly [DtsLocation, DtsLocation]; 73 | 74 | export interface DtsLocation { 75 | readonly source: string; 76 | readonly line: number; 77 | readonly col: number; 78 | } 79 | -------------------------------------------------------------------------------- /src/impl/dts-source.ts: -------------------------------------------------------------------------------- 1 | import ts from 'typescript'; 2 | import type { DtsSetup } from './dts-setup'; 3 | import { DtsSourceMap } from './dts-source-map'; 4 | 5 | export class DtsSource { 6 | static async create( 7 | sources: readonly DtsSourceFile[], 8 | setup: DtsSetup, 9 | ): Promise { 10 | let source: ts.SourceFile | undefined; 11 | let sourceMap: { path: string; content: string } | undefined; 12 | 13 | for (const { path, content } of sources) { 14 | if (path.endsWith('.d.ts')) { 15 | source = ts.createSourceFile(path, content, setup.scriptTarget, true); 16 | } else if (path.endsWith('.d.ts.map')) { 17 | sourceMap = { path, content }; 18 | } 19 | } 20 | 21 | return ( 22 | source && 23 | new DtsSource( 24 | source, 25 | sourceMap && (await DtsSourceMap.create(sourceMap.path, sourceMap.content, setup)), 26 | setup, 27 | ) 28 | ); 29 | } 30 | 31 | constructor( 32 | readonly source: ts.SourceFile, 33 | readonly map: DtsSourceMap | undefined, 34 | readonly setup: DtsSetup, 35 | ) {} 36 | 37 | destroy(): void { 38 | this.map?.destroy(); 39 | } 40 | 41 | hasMap(): this is DtsSource.WithMap { 42 | return !!this.map; 43 | } 44 | } 45 | 46 | export namespace DtsSource { 47 | export interface WithMap extends DtsSource { 48 | readonly map: DtsSourceMap; 49 | } 50 | } 51 | 52 | export interface DtsSourceFile { 53 | readonly path: string; 54 | 55 | readonly content: string; 56 | } 57 | -------------------------------------------------------------------------------- /src/impl/dts-transformer.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path'; 2 | import ts from 'typescript'; 3 | import type { FlatDts } from '../api'; 4 | import { createFlatDts } from './create-flat-dts'; 5 | import { DtsContent } from './dts-content'; 6 | import type { DtsSource } from './dts-source'; 7 | import { ModuleIndex } from './module-index'; 8 | import type { ModuleInfo } from './module-info'; 9 | import { allTransformed, noneTransformed, TopLevelStatement, Transformed } from './transformed'; 10 | 11 | export class DtsTransformer { 12 | private readonly _index: ModuleIndex; 13 | 14 | constructor(private readonly _source: DtsSource) { 15 | this._index = new ModuleIndex(_source); 16 | } 17 | 18 | async transform(initialDiagnostics: readonly ts.Diagnostic[]): Promise { 19 | const topLevel = await this._transform(); 20 | const diagnostics: ts.Diagnostic[] = initialDiagnostics.slice(); 21 | const files = this._emitFiles(topLevel, diagnostics); 22 | 23 | return createFlatDts(files, diagnostics); 24 | } 25 | 26 | private _emitFiles( 27 | statements: readonly Transformed[], 28 | diagnostics: ts.Diagnostic[], 29 | ): readonly FlatDts.File[] { 30 | const contentByPath = new Map(); 31 | 32 | for (const { to: topLevel, dia, refs } of statements) { 33 | if (dia) { 34 | diagnostics.push(...dia); 35 | } 36 | 37 | for (const [info, statement] of topLevel) { 38 | if (info.file) { 39 | const key = resolve(info.file); 40 | let dtsContent = contentByPath.get(key); 41 | 42 | if (!dtsContent) { 43 | dtsContent = new DtsContent(this._source, info); 44 | contentByPath.set(key, dtsContent); 45 | } 46 | 47 | dtsContent.refer(refs).append(statement); 48 | } 49 | } 50 | } 51 | 52 | return [...contentByPath.values()].flatMap(content => content.toFiles()); 53 | } 54 | 55 | private async _transform(): Promise[]> { 56 | return Promise.all(this._source.source.statements.map(statement => this._topLevel(statement))); 57 | } 58 | 59 | private async _topLevel(statement: ts.Statement): Promise> { 60 | if (statement.kind === ts.SyntaxKind.ModuleDeclaration) { 61 | return this._topLevelModuleDecl(statement as ts.ModuleDeclaration); 62 | } 63 | 64 | return { to: [[await this._index.main(), statement]] }; 65 | } 66 | 67 | private async _topLevelModuleDecl( 68 | decl: ts.ModuleDeclaration, 69 | ): Promise> { 70 | if (ts.isMemberName(decl.name)) { 71 | return { to: [[await this._index.main(), decl]] }; 72 | } 73 | 74 | const moduleName = decl.name.text; 75 | const target = await this._index.byName(moduleName); 76 | 77 | if (target.isExternal) { 78 | // External module remains as is. 79 | return { to: [[await this._index.main(), decl]] }; 80 | } 81 | if (target.isInternal) { 82 | // Remove internal module declarations. 83 | return noneTransformed(); 84 | } 85 | 86 | // Rename the module. 87 | const { 88 | to: [body], 89 | dia, 90 | refs, 91 | } = await this._moduleBody(target, decl); 92 | 93 | return { 94 | to: body 95 | ? [ 96 | [ 97 | target, 98 | ts.factory.updateModuleDeclaration( 99 | decl, 100 | decl.modifiers, 101 | ts.factory.createStringLiteral(target.declareAs), 102 | body, 103 | ), 104 | ], 105 | ] 106 | : [], 107 | dia, 108 | refs, 109 | }; 110 | } 111 | 112 | private async _moduleBody( 113 | enclosing: ModuleInfo, 114 | decl: ts.ModuleDeclaration, 115 | ): Promise> { 116 | const { body } = decl; 117 | 118 | if (!isBodyBlock(body)) { 119 | return noneTransformed(); 120 | } 121 | 122 | const { 123 | to: statements, 124 | dia, 125 | refs, 126 | }: Transformed = await allTransformed( 127 | body.statements.map(statement => this._statement(enclosing, statement)), 128 | ); 129 | 130 | if (!statements.length) { 131 | return noneTransformed(); 132 | } 133 | 134 | return { 135 | to: [ts.factory.updateModuleBlock(body, statements)], 136 | dia, 137 | refs, 138 | }; 139 | } 140 | 141 | private async _statement( 142 | enclosing: ModuleInfo, 143 | statement: ts.Statement, 144 | ): Promise> { 145 | switch (statement.kind) { 146 | case ts.SyntaxKind.ImportDeclaration: 147 | return this._import(enclosing, statement as ts.ImportDeclaration); 148 | case ts.SyntaxKind.ExportDeclaration: 149 | return this._export(enclosing, statement as ts.ExportDeclaration); 150 | case ts.SyntaxKind.ModuleDeclaration: 151 | return this._innerModuleDecl(enclosing, statement as ts.ModuleDeclaration); 152 | default: 153 | return { to: [statement] }; 154 | } 155 | } 156 | 157 | private async _import( 158 | enclosing: ModuleInfo, 159 | statement: ts.ImportDeclaration, 160 | ): Promise> { 161 | const { moduleSpecifier } = statement; 162 | 163 | if (moduleSpecifier.kind !== ts.SyntaxKind.StringLiteral) { 164 | // Invalid syntax? 165 | return { to: [statement] }; 166 | } 167 | 168 | const { text: from } = moduleSpecifier as ts.StringLiteral; 169 | const fromModule = await this._index.byName(from); 170 | 171 | if (fromModule.declareAs !== enclosing.declareAs) { 172 | // Import from another module. 173 | 174 | if (fromModule.isInternal) { 175 | // Drop import from internal module. 176 | return noneTransformed(); 177 | } 178 | 179 | // Replace module reference. 180 | return { 181 | to: [ 182 | ts.factory.updateImportDeclaration( 183 | statement, 184 | statement.modifiers, 185 | statement.importClause, 186 | ts.factory.createStringLiteral(fromModule.declareAs), 187 | statement.assertClause, 188 | ), 189 | ], 190 | refs: [fromModule], 191 | }; 192 | } 193 | 194 | // Import from the same module. 195 | const { importClause } = statement; 196 | 197 | if (!importClause) { 198 | // No import clause. Remove the import. 199 | return noneTransformed(); 200 | } 201 | 202 | const { name } = importClause; 203 | let { namedBindings } = importClause; 204 | 205 | if (namedBindings && namedBindings.kind === ts.SyntaxKind.NamedImports) { 206 | // Preserve aliased imports only. 207 | 208 | const elements = namedBindings.elements.filter(spec => !!spec.propertyName); 209 | 210 | if (elements.length) { 211 | namedBindings = ts.factory.updateNamedImports(namedBindings, elements); 212 | } else { 213 | namedBindings = undefined; 214 | } 215 | } 216 | 217 | if (!name && !namedBindings) { 218 | // No need in import statement. 219 | return noneTransformed(); 220 | } 221 | 222 | return { 223 | to: [ 224 | ts.factory.updateImportDeclaration( 225 | statement, 226 | statement.modifiers, 227 | ts.factory.updateImportClause(importClause, importClause.isTypeOnly, name, namedBindings), 228 | ts.factory.createStringLiteral(enclosing.declareAs), 229 | statement.assertClause, 230 | ), 231 | ], 232 | dia: name ? [this._diagnostics(statement, 'Unsupported default import')] : undefined, 233 | }; 234 | } 235 | 236 | private async _export( 237 | enclosing: ModuleInfo, 238 | statement: ts.ExportDeclaration, 239 | ): Promise> { 240 | const { moduleSpecifier } = statement; 241 | 242 | if (!moduleSpecifier) { 243 | // No module specifier. 244 | // The export remains as is. 245 | return { to: [statement] }; 246 | } 247 | if (moduleSpecifier.kind !== ts.SyntaxKind.StringLiteral) { 248 | // Invalid syntax? 249 | return { to: [statement] }; 250 | } 251 | 252 | const { text: from } = moduleSpecifier as ts.StringLiteral; 253 | const fromModule = await this._index.byName(from); 254 | 255 | if (fromModule.declareAs !== enclosing.declareAs) { 256 | // Export from another module. 257 | 258 | if (fromModule.isInternal) { 259 | // Drop export from internal module. 260 | return noneTransformed(); 261 | } 262 | 263 | // Replace module reference. 264 | return { 265 | to: [ 266 | ts.factory.updateExportDeclaration( 267 | statement, 268 | statement.modifiers, 269 | statement.isTypeOnly, 270 | statement.exportClause, 271 | ts.factory.createStringLiteral(fromModule.declareAs), 272 | statement.assertClause, 273 | ), 274 | ], 275 | refs: [fromModule], 276 | }; 277 | } 278 | 279 | // Export from the same module. 280 | const { exportClause } = statement; 281 | 282 | if (!exportClause) { 283 | // No need to re-export. 284 | return noneTransformed(); 285 | } 286 | 287 | if (exportClause.kind === ts.SyntaxKind.NamedExports) { 288 | // Preserve aliased exports only. 289 | 290 | const elements = exportClause.elements.filter(spec => !!spec.propertyName); 291 | 292 | if (!elements.length) { 293 | return noneTransformed(); 294 | } 295 | 296 | return { 297 | to: [ 298 | ts.factory.updateExportDeclaration( 299 | statement, 300 | statement.modifiers, 301 | statement.isTypeOnly, 302 | ts.factory.updateNamedExports(exportClause, elements), 303 | undefined, 304 | statement.assertClause, 305 | ), 306 | ], 307 | }; 308 | } 309 | 310 | // Namespace export. 311 | // Remains as is, but this would break the `.d.ts`. 312 | return { 313 | to: [statement], 314 | dia: [this._diagnostics(statement, 'Unsupported default export')], 315 | }; 316 | } 317 | 318 | private async _innerModuleDecl( 319 | enclosing: ModuleInfo, 320 | decl: ts.ModuleDeclaration, 321 | ): Promise> { 322 | if (ts.isMemberName(decl.name)) { 323 | return { to: [decl] }; 324 | } 325 | 326 | const moduleName = decl.name.text; 327 | const target = await this._index.byName(moduleName); 328 | 329 | if (target.isExternal) { 330 | return { to: [decl] }; 331 | } 332 | if (target.isInternal) { 333 | return noneTransformed(); 334 | } 335 | 336 | if (target === enclosing) { 337 | // No need to re-declare the module within itself. 338 | 339 | const { body } = decl; 340 | 341 | if (!isBodyBlock(body)) { 342 | return noneTransformed(); 343 | } 344 | 345 | // Expand module body. 346 | return allTransformed( 347 | body.statements.map(statement => this._statement(enclosing, statement)), 348 | ); 349 | } 350 | 351 | // Rename the module. 352 | const { 353 | to: [body], 354 | dia, 355 | refs, 356 | } = await this._moduleBody(target, decl); 357 | 358 | return { 359 | to: body 360 | ? [ 361 | ts.factory.updateModuleDeclaration( 362 | decl, 363 | decl.modifiers, 364 | ts.factory.createStringLiteral(target.declareAs), 365 | body, 366 | ), 367 | ] 368 | : [], 369 | dia, 370 | refs, 371 | }; 372 | } 373 | 374 | private _diagnostics(node: ts.Node, messageText: string): ts.DiagnosticWithLocation { 375 | const { source } = this._source; 376 | const start = node.getStart(source); 377 | const end = node.getEnd(); 378 | 379 | return { 380 | category: ts.DiagnosticCategory.Error, 381 | code: 9999, 382 | file: source, 383 | start: start, 384 | length: end - start, 385 | messageText, 386 | }; 387 | } 388 | } 389 | 390 | function isBodyBlock(body: ts.ModuleDeclaration['body']): body is ts.ModuleBlock { 391 | return !!body && ts.isModuleBlock(body); 392 | } 393 | -------------------------------------------------------------------------------- /src/impl/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dts-setup'; 2 | export * from './dts-source'; 3 | export * from './dts-transformer'; 4 | export * from './create-flat-dts'; 5 | export * from './transformed'; 6 | -------------------------------------------------------------------------------- /src/impl/module-index.ts: -------------------------------------------------------------------------------- 1 | import type { FlatDts } from '../api'; 2 | import { DtsMeta } from './dts-meta'; 3 | import type { DtsSource } from './dts-source'; 4 | import { ModuleInfo } from './module-info'; 5 | import { moduleMatcher } from './module-matcher'; 6 | 7 | export class ModuleIndex { 8 | private readonly _meta: DtsMeta; 9 | private readonly _isInternal: (name: string) => boolean; 10 | private readonly _isExternal: (name: string) => boolean; 11 | private readonly _names: readonly string[]; 12 | private readonly _declarations: ReadonlyMap; 13 | private readonly _byName = new Map>(); 14 | 15 | constructor(private readonly _source: DtsSource) { 16 | const { 17 | source, 18 | setup: { dtsOptions }, 19 | } = _source; 20 | const { entries = {} } = dtsOptions; 21 | 22 | this._meta = new DtsMeta(source); 23 | 24 | const names: string[] = []; 25 | const declarations = new Map(); 26 | 27 | for (const [name, decl] of Object.entries(entries)) { 28 | if (decl) { 29 | declarations.set(name, decl); 30 | names.push(name); 31 | } 32 | } 33 | 34 | // Longest first. 35 | names.sort((first, second) => second.length - first.length); 36 | 37 | this._names = names; 38 | this._declarations = declarations; 39 | this._isInternal = moduleMatcher(dtsOptions.internal); 40 | this._isExternal = moduleMatcher(dtsOptions.external); 41 | } 42 | 43 | byName(name: string): Promise { 44 | const found = this._byName.get(name); 45 | 46 | if (found) { 47 | // Already cached. 48 | return found; 49 | } 50 | if (this._isExternal(name)) { 51 | // External module. 52 | return this._put(name, ModuleInfo.external(this._source, name)); 53 | } 54 | if (this._isInternal(name)) { 55 | // External module. 56 | return this._put(name, ModuleInfo.internal(this._source, name)); 57 | } 58 | 59 | const decl = this._declarations.get(name); 60 | 61 | if (decl) { 62 | // Entry declared. 63 | return this._put( 64 | name, 65 | this.main().then(main => main.nested(name, decl)), 66 | ); 67 | } 68 | if (!this._meta.isModuleDeclared(name)) { 69 | // No such module declaration in `.d.ts` file. 70 | // Mark it external. 71 | return this._put(name, ModuleInfo.external(this._source, name)); 72 | } 73 | 74 | const matchingName = this._names.find(n => n === name || name.startsWith(n + '/')); 75 | 76 | if (matchingName) { 77 | // Use matching module. 78 | return this._put(name, this.byName(matchingName)); 79 | } 80 | 81 | // No matching module found. 82 | // Return the main entry. 83 | return this.main(); 84 | } 85 | 86 | main(): Promise { 87 | const main = ModuleInfo.main(this._source); 88 | 89 | return (this.main = () => main)(); 90 | } 91 | 92 | private _put(name: string, info: ModuleInfo | PromiseLike): Promise { 93 | const result = Promise.resolve(info); 94 | 95 | this._byName.set(name, result); 96 | 97 | return result; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/impl/module-info.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'node:fs'; 2 | import path from 'node:path'; 3 | import type { FlatDts } from '../api'; 4 | import type { DtsPrinter } from './dts-printer'; 5 | import type { DtsSource } from './dts-source'; 6 | 7 | const noReferredLibs: ReadonlySet = /*#__PURE__*/ new Set(); 8 | 9 | export class ModuleInfo { 10 | static async main(source: DtsSource): Promise { 11 | const { 12 | source: { fileName: file }, 13 | setup: { 14 | dtsOptions: { moduleName = await packageName(), lib, refs = true }, 15 | }, 16 | } = source; 17 | 18 | return new ModuleInfo(source, moduleName, { 19 | file, 20 | libs: referredLibs(source, lib), 21 | refs, 22 | }); 23 | } 24 | 25 | static external(source: DtsSource, name: string): ModuleInfo { 26 | return new ModuleInfo(source, name, 'external'); 27 | } 28 | 29 | static internal(source: DtsSource, name: string): ModuleInfo { 30 | return new ModuleInfo(source, name, 'internal'); 31 | } 32 | 33 | readonly isExternal: boolean; 34 | readonly isInternal: boolean; 35 | readonly file: string | undefined; 36 | readonly refs: boolean; 37 | private readonly _libs: ReadonlySet; 38 | 39 | private constructor( 40 | readonly source: DtsSource, 41 | readonly declareAs: string, 42 | kind: 43 | | 'internal' 44 | | 'external' 45 | | { 46 | file: string; 47 | libs: ReadonlySet; 48 | refs: boolean; 49 | }, 50 | ) { 51 | if (typeof kind === 'string') { 52 | this.isExternal = kind === 'external'; 53 | this.isInternal = !this.isExternal; 54 | this.file = undefined; 55 | this.refs = false; 56 | this._libs = noReferredLibs; 57 | } else { 58 | this.isExternal = false; 59 | this.isInternal = false; 60 | this.file = kind.file; 61 | this.refs = kind.refs; 62 | this._libs = kind.libs; 63 | } 64 | } 65 | 66 | prelude(printer: DtsPrinter): void { 67 | for (const lib of this._libs) { 68 | printer.text(`/// `).nl(); 69 | } 70 | } 71 | 72 | nested(name: string, decl: FlatDts.EntryDecl): ModuleInfo { 73 | let { as: declareAs = name } = decl; 74 | 75 | if (this.declareAs) { 76 | declareAs = `${this.declareAs}/${declareAs}`; 77 | } 78 | if (declareAs) { 79 | // Nested entry name. 80 | return new ModuleInfo(this.source, declareAs, { 81 | file: decl.file ?? this.file!, 82 | libs: referredLibs(this.source, decl.lib, this._libs), 83 | refs: decl.refs ?? this.refs, 84 | }); 85 | } 86 | 87 | return this; 88 | } 89 | 90 | pathTo({ file: to }: ModuleInfo): string | undefined { 91 | const from = this.file; 92 | 93 | if (!from || !to || from === to) { 94 | return; 95 | } 96 | 97 | const relativePath = path.relative(path.dirname(from), to); 98 | 99 | return relativePath.split(path.sep).map(encodeURIComponent).join(path.sep); 100 | } 101 | } 102 | 103 | async function packageName(): Promise { 104 | const packageJson = await fs.readFile('package.json', { encoding: 'utf-8' }); 105 | const { name } = JSON.parse(packageJson) as { name?: string | undefined }; 106 | 107 | if (!name) { 108 | throw new Error( 109 | 'Can not detect module name automatically. ' + 110 | "Consider to set `flatDts({ moduleName: '' })` option explicitly", 111 | ); 112 | } 113 | 114 | return name; 115 | } 116 | 117 | function referredLibs( 118 | source: DtsSource, 119 | lib: FlatDts.Options['lib'], 120 | defaultLibs = noReferredLibs, 121 | ): ReadonlySet { 122 | if (lib === true) { 123 | lib = source.setup.compilerOptions.lib; 124 | } 125 | if (lib == null) { 126 | return defaultLibs; 127 | } 128 | 129 | const result = new Set(); 130 | 131 | if (typeof lib === 'string') { 132 | result.add(referredLib(lib)); 133 | } else if (lib !== false) { 134 | for (const name of lib) { 135 | result.add(referredLib(name)); 136 | } 137 | } 138 | 139 | return result; 140 | } 141 | 142 | function referredLib(name: string): string { 143 | return name.endsWith('.d.ts') && name.startsWith('lib.') ? name.slice(4, -5) : name; 144 | } 145 | -------------------------------------------------------------------------------- /src/impl/module-matcher.ts: -------------------------------------------------------------------------------- 1 | import isGlob from 'is-glob'; 2 | import micromatch from 'micromatch'; 3 | 4 | export function moduleMatcher( 5 | patterns: string | readonly string[] | undefined, 6 | ): (name: string) => boolean { 7 | const globs = patternsToGlobs(patterns); 8 | 9 | if (!globs.length) { 10 | return _name => false; 11 | } 12 | 13 | return name => micromatch.isMatch(name, globs, { dot: true }); 14 | } 15 | 16 | function patternsToGlobs(patterns: string | readonly string[] | undefined): readonly string[] { 17 | return patterns 18 | ? Array.isArray(patterns) 19 | ? (patterns as readonly string[]).map(patternToGlob) 20 | : [patternToGlob(patterns as string)] 21 | : []; 22 | } 23 | 24 | function patternToGlob(pattern: string): string { 25 | return isGlob(pattern, { strict: false }) ? pattern : `${pattern}/**`; 26 | } 27 | -------------------------------------------------------------------------------- /src/impl/simple-dts-printer.ts: -------------------------------------------------------------------------------- 1 | import type { FlatDts } from '../api'; 2 | import { DtsPrinter } from './dts-printer'; 3 | 4 | export class SimpleDtsPrinter extends DtsPrinter { 5 | toFiles(name: string): readonly FlatDts.File[] { 6 | return [this.createFile(name)]; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/impl/source-map-dts-printer.ts: -------------------------------------------------------------------------------- 1 | import type ts from 'typescript'; 2 | import type { FlatDts } from '../api'; 3 | import { DtsMapper } from './dts-mapper'; 4 | import { DtsPrinter } from './dts-printer'; 5 | import type { DtsSource } from './dts-source'; 6 | 7 | export class SourceMapDtsPrinter extends DtsPrinter { 8 | private readonly _nodes: ts.Node[] = []; 9 | 10 | print(node: ts.Node): this { 11 | this._nodes.push(node); 12 | 13 | return super.print(node); 14 | } 15 | 16 | toFiles(name: string): readonly FlatDts.File[] { 17 | const dts = this.createFile(name); 18 | const sourceMap = this._createSourceMapFile(dts); 19 | const { setup } = this.source; 20 | 21 | return [ 22 | this.createFile( 23 | dts.path, 24 | `${dts.content}//# sourceMappingURL=${setup.basename(sourceMap.path)}${setup.eol}`, 25 | ), 26 | sourceMap, 27 | ]; 28 | } 29 | 30 | private _createSourceMapFile(dtsFile: FlatDts.File): FlatDts.File { 31 | return this.createFile( 32 | `${dtsFile.path}.map`, 33 | new DtsMapper(this.source, dtsFile).map(this._nodes).toString(), 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/impl/transformed.ts: -------------------------------------------------------------------------------- 1 | import type ts from 'typescript'; 2 | import type { ModuleInfo } from './module-info'; 3 | 4 | export interface Transformed { 5 | readonly to: T | []; 6 | readonly dia?: ts.Diagnostic[] | undefined; 7 | readonly refs?: ModuleInfo[] | undefined; 8 | } 9 | 10 | export type TopLevelStatement = readonly [target: ModuleInfo, statement: ts.Statement]; 11 | 12 | const NONE_TRANSFORMED: Transformed = { to: [] }; 13 | 14 | export function noneTransformed(): Transformed { 15 | return NONE_TRANSFORMED as Transformed; 16 | } 17 | 18 | export async function allTransformed( 19 | transformed: Promise>[], 20 | ): Promise> { 21 | const list = await Promise.all(transformed); 22 | 23 | return list.reduce( 24 | ({ to: all, dia: fullDia, refs: allRefs }, { to, dia, refs }) => ({ 25 | to: [...all, ...to], 26 | dia: dia ? (fullDia ? [...fullDia, ...dia] : dia) : fullDia, 27 | refs: refs ? (allRefs ? [...allRefs, ...refs] : refs) : allRefs, 28 | }), 29 | noneTransformed(), 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/plugin.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module rollup-plugin-flat-dts 3 | */ 4 | import { dirname, relative, resolve } from 'node:path'; 5 | import type { OutputPlugin } from 'rollup'; 6 | import type { FlatDts } from './api'; 7 | import { emitFlatDts } from './api'; 8 | 9 | export type { FlatDts }; 10 | 11 | /** 12 | * Creates type definitions flattening plugin instance. 13 | * 14 | * @param dtsOptions - Type definition flattening options. 15 | * 16 | * @returns Rollup output plugin instance. 17 | */ 18 | export default function flatDtsPlugin(dtsOptions?: FlatDts.Options): OutputPlugin { 19 | return { 20 | name: 'flat-dts', 21 | 22 | async generateBundle({ dir, file }): Promise { 23 | let assetPath = (filePath: string): string => filePath; 24 | 25 | if (file != null) { 26 | dir = dirname(file); 27 | } 28 | 29 | if (dir != null) { 30 | dtsOptions = dtsOptionsRelativeToDir(dir, dtsOptions); 31 | assetPath = filePath => relative(dir, filePath); 32 | } 33 | 34 | const dts = await emitFlatDts(dtsOptions); 35 | 36 | if (dts.diagnostics.length) { 37 | this.error(dts.formatDiagnostics()); 38 | } 39 | 40 | dts.files.forEach(({ path, content }) => { 41 | this.emitFile({ 42 | type: 'asset', 43 | fileName: assetPath(path), 44 | source: content, 45 | }); 46 | }); 47 | }, 48 | }; 49 | } 50 | 51 | function dtsOptionsRelativeToDir(dir: string, dtsOptions: FlatDts.Options = {}): FlatDts.Options { 52 | const { file = 'index.d.ts', entries = {} } = dtsOptions; 53 | 54 | return { 55 | ...dtsOptions, 56 | file: relative(process.cwd(), resolve(dir, file)), 57 | entries: Object.fromEntries( 58 | Object.entries(entries).map(([key, dtsEntry = {}]) => [ 59 | key, 60 | dtsEntryRelativeToDir(dir, dtsEntry), 61 | ]), 62 | ), 63 | }; 64 | } 65 | 66 | function dtsEntryRelativeToDir(dir: string, dtsEntry: FlatDts.EntryDecl = {}): FlatDts.EntryDecl { 67 | const { file } = dtsEntry; 68 | 69 | if (file == null) { 70 | return dtsEntry; 71 | } 72 | 73 | return { 74 | ...dtsEntry, 75 | file: relative(process.cwd(), resolve(dir, file)), 76 | }; 77 | } 78 | -------------------------------------------------------------------------------- /src/tests/classes/__snapshots__/classes.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`For class emits declaration map: dts 1`] = ` 4 | "declare module "test-package" { 5 | export class BaseClass { 6 | foo(): string; 7 | } 8 | export class TestClass extends BaseClass { 9 | foo(): string; 10 | } 11 | } 12 | //# sourceMappingURL=index.d.ts.map 13 | " 14 | `; 15 | 16 | exports[`For class emits declaration map: source map 1`] = `"{"version":3,"sources":["src/tests/classes/classes.ts"],"names":[],"mappings":";IAAA,MAAM,OAAO,SAAS;QACpB,GAAG,IAAI,MAAM,CAAA;KAGd;IAED,MAAM,OAAO,SAAU,CAAA,QAAQ,SAAS;QAC7B,GAAG,IAAI,MAAM,CAAA;KAGvB","file":"index.d.ts","sourceRoot":""}"`; 17 | 18 | exports[`For class emits type definitions 1`] = ` 19 | "declare module "test-package" { 20 | export class BaseClass { 21 | foo(): string; 22 | } 23 | export class TestClass extends BaseClass { 24 | foo(): string; 25 | } 26 | } 27 | " 28 | `; 29 | -------------------------------------------------------------------------------- /src/tests/classes/classes.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@jest/globals'; 2 | import { testDts } from '../test-dts'; 3 | 4 | describe('For class', () => { 5 | it('emits type definitions', async () => { 6 | const { 7 | files: [{ content }], 8 | } = await testDts('classes'); 9 | 10 | expect(content).toMatchSnapshot(); 11 | }); 12 | it('emits declaration map', async () => { 13 | const { files } = await testDts('classes', { compilerOptions: { declarationMap: true } }); 14 | 15 | expect(files).toHaveLength(2); 16 | 17 | const [dts, sourceMap] = files; 18 | 19 | expect(dts.path).toBe('index.d.ts'); 20 | expect(sourceMap.path).toBe('index.d.ts.map'); 21 | 22 | expect(dts.content).toMatchSnapshot('dts'); 23 | expect(sourceMap.content).toMatchSnapshot('source map'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/tests/classes/classes.ts: -------------------------------------------------------------------------------- 1 | export class BaseClass { 2 | foo(): string { 3 | return 'foo'; 4 | } 5 | } 6 | 7 | export class TestClass extends BaseClass { 8 | override foo(): string { 9 | return 'bar'; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/tests/classes/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "files": ["./classes.ts"], 4 | "include": [] 5 | } 6 | -------------------------------------------------------------------------------- /src/tests/constants/__snapshots__/constants.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`For constants emits declaration map: dts 1`] = ` 4 | "declare module "test-package" { 5 | export function testFunction(prefix: string, ...args: string[]): string; 6 | } 7 | //# sourceMappingURL=index.d.ts.map 8 | " 9 | `; 10 | 11 | exports[`For constants emits declaration map: source map 1`] = `"{"version":3,"sources":["src/tests/functions/functions.ts"],"names":[],"mappings":";IAAA,MAAM,UAAU,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAEtE","file":"index.d.ts","sourceRoot":""}"`; 12 | 13 | exports[`For constants emits type definitions 1`] = ` 14 | "declare module "test-package" { 15 | export function testFunction(prefix: string, ...args: string[]): string; 16 | } 17 | " 18 | `; 19 | -------------------------------------------------------------------------------- /src/tests/constants/constants.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@jest/globals'; 2 | import { testDts } from '../test-dts'; 3 | 4 | describe('For constants', () => { 5 | it('emits type definitions', async () => { 6 | const { 7 | files: [{ content }], 8 | } = await testDts('functions'); 9 | 10 | expect(content).toMatchSnapshot(); 11 | }); 12 | it('emits declaration map', async () => { 13 | const { files } = await testDts('functions', { compilerOptions: { declarationMap: true } }); 14 | 15 | expect(files).toHaveLength(2); 16 | 17 | const [dts, sourceMap] = files; 18 | 19 | expect(dts.path).toBe('index.d.ts'); 20 | expect(sourceMap.path).toBe('index.d.ts.map'); 21 | 22 | expect(dts.content).toMatchSnapshot('dts'); 23 | expect(sourceMap.content).toMatchSnapshot('source map'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/tests/constants/constants.ts: -------------------------------------------------------------------------------- 1 | export const foo = 'abc'; 2 | export const bar: number = 123 + 456; 3 | -------------------------------------------------------------------------------- /src/tests/constants/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "files": ["./constants.ts"], 4 | "include": [] 5 | } 6 | -------------------------------------------------------------------------------- /src/tests/emit-flat-dts.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@jest/globals'; 2 | import { testDts } from './test-dts'; 3 | 4 | describe('emitFlatDts', () => { 5 | describe('file.path', () => { 6 | it('defaults to `index.d.ts`', async () => { 7 | const { 8 | files: [{ path }], 9 | } = await testDts('interfaces'); 10 | 11 | expect(path).toBe('index.d.ts'); 12 | }); 13 | it('respects custom file name', async () => { 14 | const { 15 | files: [{ path }], 16 | } = await testDts('interfaces', { file: 'custom.d.ts' }); 17 | 18 | expect(path).toBe('custom.d.ts'); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/tests/entries/__snapshots__/entries.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`With multiple entries emits multiple definition files when entry file name specified: file1 1`] = ` 4 | "declare module "test-package/entry1" { 5 | export interface Entry1 { 6 | readonly name: 'entry1'; 7 | } 8 | } 9 | " 10 | `; 11 | 12 | exports[`With multiple entries emits multiple definition files when entry file name specified: file2 1`] = ` 13 | "declare module "test-package/entry2" { 14 | export interface Entry2 { 15 | readonly name: 'entry2'; 16 | } 17 | } 18 | " 19 | `; 20 | 21 | exports[`With multiple entries emits multiple definition files when entry file name specified: file3 1`] = ` 22 | "/// 23 | /// 24 | declare module "test-package" { 25 | export * from "test-package/entry1"; 26 | export * from "test-package/entry2"; 27 | export interface Root { 28 | readonly name: 'root'; 29 | } 30 | } 31 | " 32 | `; 33 | 34 | exports[`With multiple entries emits one definition file when entry file name omitted 1`] = ` 35 | "declare module "test-package/entry1" { 36 | export interface Entry1 { 37 | readonly name: 'entry1'; 38 | } 39 | } 40 | 41 | declare module "test-package/entry2" { 42 | export interface Entry2 { 43 | readonly name: 'entry2'; 44 | } 45 | } 46 | 47 | declare module "test-package" { 48 | export * from "test-package/entry1"; 49 | export * from "test-package/entry2"; 50 | export interface Root { 51 | readonly name: 'root'; 52 | } 53 | } 54 | " 55 | `; 56 | 57 | exports[`With multiple entries merges multiple entries with the same file name: file1 1`] = ` 58 | "declare module "test-package/entry1" { 59 | export interface Entry1 { 60 | readonly name: 'entry1'; 61 | } 62 | } 63 | 64 | declare module "test-package/entry2" { 65 | export interface Entry2 { 66 | readonly name: 'entry2'; 67 | } 68 | } 69 | " 70 | `; 71 | 72 | exports[`With multiple entries merges multiple entries with the same file name: file2 1`] = ` 73 | "/// 74 | /// 75 | declare module "test-package" { 76 | export * from "test-package/entry1"; 77 | export * from "test-package/entry2"; 78 | export interface Root { 79 | readonly name: 'root'; 80 | } 81 | } 82 | " 83 | `; 84 | 85 | exports[`With multiple entries renames the entry when its final name specified 1`] = ` 86 | "declare module "test-package/test-entry1" { 87 | export interface Entry1 { 88 | readonly name: 'entry1'; 89 | } 90 | } 91 | 92 | declare module "test-package/test-entry2" { 93 | export interface Entry2 { 94 | readonly name: 'entry2'; 95 | } 96 | } 97 | 98 | declare module "test-package" { 99 | export * from "test-package/test-entry1"; 100 | export * from "test-package/test-entry2"; 101 | export interface Root { 102 | readonly name: 'root'; 103 | } 104 | } 105 | " 106 | `; 107 | -------------------------------------------------------------------------------- /src/tests/entries/entries.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@jest/globals'; 2 | import { testDts } from '../test-dts'; 3 | 4 | describe('With multiple entries', () => { 5 | it('emits multiple definition files when entry file name specified', async () => { 6 | const { files } = await testDts('entries', { 7 | entries: { 8 | entry1: { file: 'entry1.d.ts' }, 9 | entry2: { file: 'entry2.d.ts' }, 10 | }, 11 | }); 12 | 13 | expect(files).toHaveLength(3); 14 | 15 | const [{ content: file1 }, { content: file2 }, { content: file3 }] = files; 16 | 17 | expect(file1).toMatchSnapshot('file1'); 18 | expect(file2).toMatchSnapshot('file2'); 19 | expect(file3).toMatchSnapshot('file3'); 20 | }); 21 | it('emits one definition file when entry file name omitted', async () => { 22 | const { files } = await testDts('entries', { 23 | entries: { 24 | entry1: {}, 25 | entry2: {}, 26 | }, 27 | }); 28 | 29 | expect(files).toHaveLength(1); 30 | 31 | const [{ content }] = files; 32 | 33 | expect(content).toMatchSnapshot(); 34 | }); 35 | it('renames the entry when its final name specified', async () => { 36 | const { files } = await testDts('entries', { 37 | entries: { 38 | entry1: { as: 'test-entry1' }, 39 | entry2: { as: 'test-entry2' }, 40 | }, 41 | }); 42 | 43 | expect(files).toHaveLength(1); 44 | 45 | const [{ content }] = files; 46 | 47 | expect(content).toMatchSnapshot(); 48 | }); 49 | it('merges multiple entries with the same file name', async () => { 50 | const { files } = await testDts('entries', { 51 | entries: { 52 | entry1: { file: 'entries.d.ts' }, 53 | entry2: { file: 'entries.d.ts' }, 54 | }, 55 | }); 56 | 57 | expect(files).toHaveLength(2); 58 | 59 | const [{ content: file1 }, { content: file2 }] = files; 60 | 61 | expect(file1).toMatchSnapshot('file1'); 62 | expect(file2).toMatchSnapshot('file2'); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/tests/entries/entry1/index.ts: -------------------------------------------------------------------------------- 1 | export interface Entry1 { 2 | readonly name: 'entry1'; 3 | } 4 | -------------------------------------------------------------------------------- /src/tests/entries/entry2/index.ts: -------------------------------------------------------------------------------- 1 | export interface Entry2 { 2 | readonly name: 'entry2'; 3 | } 4 | -------------------------------------------------------------------------------- /src/tests/entries/root.ts: -------------------------------------------------------------------------------- 1 | export * from './entry1'; 2 | export * from './entry2'; 3 | 4 | export interface Root { 5 | readonly name: 'root'; 6 | } 7 | -------------------------------------------------------------------------------- /src/tests/entries/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "files": ["./root.ts"], 4 | "include": [] 5 | } 6 | -------------------------------------------------------------------------------- /src/tests/functions/__snapshots__/functions.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`For functions emits declaration map: dts 1`] = ` 4 | "declare module "test-package" { 5 | export function testFunction(prefix: string, ...args: string[]): string; 6 | } 7 | //# sourceMappingURL=index.d.ts.map 8 | " 9 | `; 10 | 11 | exports[`For functions emits declaration map: source map 1`] = `"{"version":3,"sources":["src/tests/functions/functions.ts"],"names":[],"mappings":";IAAA,MAAM,UAAU,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAEtE","file":"index.d.ts","sourceRoot":""}"`; 12 | 13 | exports[`For functions emits type definitions 1`] = ` 14 | "declare module "test-package" { 15 | export function testFunction(prefix: string, ...args: string[]): string; 16 | } 17 | " 18 | `; 19 | -------------------------------------------------------------------------------- /src/tests/functions/functions.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@jest/globals'; 2 | import { testDts } from '../test-dts'; 3 | 4 | describe('For functions', () => { 5 | it('emits type definitions', async () => { 6 | const { 7 | files: [{ content }], 8 | } = await testDts('functions'); 9 | 10 | expect(content).toMatchSnapshot(); 11 | }); 12 | it('emits declaration map', async () => { 13 | const { files } = await testDts('functions', { compilerOptions: { declarationMap: true } }); 14 | 15 | expect(files).toHaveLength(2); 16 | 17 | const [dts, sourceMap] = files; 18 | 19 | expect(dts.path).toBe('index.d.ts'); 20 | expect(sourceMap.path).toBe('index.d.ts.map'); 21 | 22 | expect(dts.content).toMatchSnapshot('dts'); 23 | expect(sourceMap.content).toMatchSnapshot('source map'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/tests/functions/functions.ts: -------------------------------------------------------------------------------- 1 | export function testFunction(prefix: string, ...args: string[]): string { 2 | return `${prefix}: ` + args.join(','); 3 | } 4 | -------------------------------------------------------------------------------- /src/tests/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "files": ["./functions.ts"], 4 | "include": [] 5 | } 6 | -------------------------------------------------------------------------------- /src/tests/imports/__snapshots__/imports.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`For imports with \`.js\` extension emits declaration map: dts 1`] = ` 4 | "declare module "test-package/imported" { 5 | export interface Imported { 6 | readonly name: 'imported'; 7 | } 8 | } 9 | 10 | declare module "test-package" { 11 | export * from "test-package/imported"; 12 | } 13 | //# sourceMappingURL=index.d.ts.map 14 | " 15 | `; 16 | 17 | exports[`For imports with \`.js\` extension emits declaration map: source map 1`] = `"{"version":3,"sources":["src/tests/imports/imported.ts","src/tests/imports/importer.ts"],"names":[],"mappings":";IAAA,MAAM,WAAW,QAAQ;QACvB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;KAC3B;;;;ICFD,sCAA8B","file":"index.d.ts","sourceRoot":""}"`; 18 | 19 | exports[`For imports with \`.js\` extension emits type definitions 1`] = ` 20 | "declare module "test-package/imported" { 21 | export interface Imported { 22 | readonly name: 'imported'; 23 | } 24 | } 25 | 26 | declare module "test-package" { 27 | export * from "test-package/imported"; 28 | } 29 | " 30 | `; 31 | -------------------------------------------------------------------------------- /src/tests/imports/imported.ts: -------------------------------------------------------------------------------- 1 | export interface Imported { 2 | readonly name: 'imported'; 3 | } 4 | -------------------------------------------------------------------------------- /src/tests/imports/importer.ts: -------------------------------------------------------------------------------- 1 | export * from './imported.js'; 2 | -------------------------------------------------------------------------------- /src/tests/imports/imports.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@jest/globals'; 2 | import { testDts } from '../test-dts'; 3 | 4 | describe('For imports with `.js` extension', () => { 5 | it('emits type definitions', async () => { 6 | const { 7 | files: [{ content }], 8 | } = await testDts('imports', { 9 | entries: { imported: {} }, 10 | }); 11 | 12 | expect(content).toMatchSnapshot(); 13 | }); 14 | it('emits declaration map', async () => { 15 | const { files } = await testDts('imports', { 16 | entries: { imported: {} }, 17 | compilerOptions: { declarationMap: true }, 18 | }); 19 | 20 | expect(files).toHaveLength(2); 21 | 22 | const [dts, sourceMap] = files; 23 | 24 | expect(dts.path).toBe('index.d.ts'); 25 | expect(sourceMap.path).toBe('index.d.ts.map'); 26 | 27 | expect(dts.content).toMatchSnapshot('dts'); 28 | expect(sourceMap.content).toMatchSnapshot('source map'); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/tests/imports/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext" 5 | }, 6 | "files": ["./importer.ts"], 7 | "include": [] 8 | } 9 | -------------------------------------------------------------------------------- /src/tests/interfaces/__snapshots__/interfaces.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`For interfaces emits declaration map: dts 1`] = ` 4 | "declare module "test-package" { 5 | export interface TestInterface { 6 | readonly foo: string; 7 | } 8 | } 9 | //# sourceMappingURL=index.d.ts.map 10 | " 11 | `; 12 | 13 | exports[`For interfaces emits declaration map: source map 1`] = `"{"version":3,"sources":["src/tests/interfaces/interfaces.ts"],"names":[],"mappings":";IAAA,MAAM,WAAW,aAAa;QAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;KACtB","file":"index.d.ts","sourceRoot":""}"`; 14 | 15 | exports[`For interfaces emits type definitions 1`] = ` 16 | "declare module "test-package" { 17 | export interface TestInterface { 18 | readonly foo: string; 19 | } 20 | } 21 | " 22 | `; 23 | -------------------------------------------------------------------------------- /src/tests/interfaces/interfaces.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@jest/globals'; 2 | import { testDts } from '../test-dts'; 3 | 4 | describe('For interfaces', () => { 5 | it('emits type definitions', async () => { 6 | const { 7 | files: [{ content }], 8 | } = await testDts('interfaces'); 9 | 10 | expect(content).toMatchSnapshot(); 11 | }); 12 | it('emits declaration map', async () => { 13 | const { files } = await testDts('interfaces', { compilerOptions: { declarationMap: true } }); 14 | 15 | expect(files).toHaveLength(2); 16 | 17 | const [dts, sourceMap] = files; 18 | 19 | expect(dts.path).toBe('index.d.ts'); 20 | expect(sourceMap.path).toBe('index.d.ts.map'); 21 | 22 | expect(dts.content).toMatchSnapshot('dts'); 23 | expect(sourceMap.content).toMatchSnapshot('source map'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/tests/interfaces/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface TestInterface { 2 | readonly foo: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/tests/interfaces/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "files": ["./interfaces.ts"], 4 | "include": [] 5 | } 6 | -------------------------------------------------------------------------------- /src/tests/test-dts.ts: -------------------------------------------------------------------------------- 1 | import type { FlatDts } from '../api'; 2 | import { emitFlatDts } from '../api'; 3 | 4 | export function testDts(root: string, options?: FlatDts.Options): Promise { 5 | return emitFlatDts({ 6 | tsconfig: `src/tests/${root}/tsconfig.json`, 7 | moduleName: 'test-package', 8 | ...options, 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "Node", 4 | "module": "ES2015", 5 | "target": "ES2019", 6 | "strict": true, 7 | "noFallthroughCasesInSwitch": true, 8 | "noImplicitReturns": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "importHelpers": true, 13 | "allowSyntheticDefaultImports": true, 14 | "noEmitHelpers": true, 15 | "lib": ["ES2019"], 16 | "types": ["node"], 17 | "outDir": "target/js", 18 | "sourceMap": true, 19 | "newLine": "LF" 20 | }, 21 | "include": ["src/**/*"] 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.main.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "files": ["./src/plugin.ts"], 4 | "include": [] 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "esModuleInterop": true, 5 | "noUnusedLocals": false, 6 | "noUnusedParameters": false 7 | }, 8 | "include": ["./src/tests/**/*.spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": ["src/plugin.ts", "src/api/index.ts"], 3 | "name": ".d.ts Flattener", 4 | "out": "./target/typedoc", 5 | "sort": ["static-first", "visibility", "enum-value-ascending", "alphabetical", "kind"] 6 | } 7 | --------------------------------------------------------------------------------