├── global.d.ts ├── test ├── sources │ ├── js │ │ ├── xxx.js │ │ ├── package.json │ │ ├── index.js │ │ └── yyy.js │ └── default │ │ ├── package.json │ │ ├── src │ │ ├── a.schema.ts │ │ ├── interfaces.ts │ │ ├── c │ │ │ └── index.ts │ │ ├── b.ts │ │ └── a.ts │ │ └── index.ts └── tests │ ├── multi-platform.test.ts │ └── e2e.test.ts ├── index.ts ├── cli.js ├── .gitignore ├── .prettierrc ├── scripts ├── fixCliLineEndings.js └── nodeModules.js ├── .npmignore ├── jest.config.js ├── tsconfig.json ├── tsconfig.test.json ├── .vscode ├── tasks.json ├── launch.json └── settings.json ├── tslint.json ├── LICENSE ├── webpack.config.dev.js ├── webpack.config.js ├── package.json ├── lib ├── log.ts ├── cli.ts └── generator.ts └── README.md /global.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'npm-run' 2 | -------------------------------------------------------------------------------- /test/sources/js/xxx.js: -------------------------------------------------------------------------------- 1 | exports.XXX = class {} 2 | -------------------------------------------------------------------------------- /test/sources/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-js" 3 | } 4 | -------------------------------------------------------------------------------- /test/sources/default/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-default" 3 | } 4 | -------------------------------------------------------------------------------- /test/sources/default/src/a.schema.ts: -------------------------------------------------------------------------------- 1 | export interface ASchema { 2 | test: string 3 | } 4 | -------------------------------------------------------------------------------- /test/sources/default/src/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface ISuggestedText { 2 | value: string 3 | } 4 | -------------------------------------------------------------------------------- /test/sources/js/index.js: -------------------------------------------------------------------------------- 1 | exports.XXX = require('./xxx').XXX 2 | exports.YYY = require('./yyy').YYY 3 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export {Generator} from './lib/generator' 2 | export {INpmDtsArgs} from './lib/cli' 3 | export {ELogLevel} from './lib/log' 4 | -------------------------------------------------------------------------------- /test/sources/js/yyy.js: -------------------------------------------------------------------------------- 1 | const {XXX} = require('./xxx') 2 | 3 | exports.YYY = class { 4 | constructor() { 5 | this.yyy = new XXX() 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | new (require('./dist/index').Generator)(undefined, null, true) 3 | .generate() 4 | .catch(e => { 5 | process.exit(1) 6 | }) 7 | -------------------------------------------------------------------------------- /test/sources/default/index.ts: -------------------------------------------------------------------------------- 1 | import {A} from './src/a' 2 | 3 | export * from './src/a' 4 | export * from './src/c' 5 | export * from './src/a.schema' 6 | 7 | A.test(A.getText().value) 8 | -------------------------------------------------------------------------------- /test/sources/default/src/c/index.ts: -------------------------------------------------------------------------------- 1 | export class C { 2 | public static test(text: IText) { 3 | process.stdout.write(`\nC: ${text}\n`) 4 | } 5 | } 6 | 7 | export type IText = string 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Add any directories, files, or patterns you don't want to be tracked by version control 2 | .git 3 | node_modules 4 | index.d.ts 5 | test.d.ts 6 | dist 7 | cache 8 | out 9 | tmp 10 | docs 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "semi": false, 7 | "bracketSpacing": false, 8 | "arrowParens": "avoid" 9 | } 10 | -------------------------------------------------------------------------------- /test/sources/default/src/b.ts: -------------------------------------------------------------------------------- 1 | import {C, IText} from './c' 2 | 3 | export class B extends C { 4 | public static test(text: IText) { 5 | super.test(text) 6 | process.stdout.write(`\nB: ${text}\n`) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /scripts/fixCliLineEndings.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const filePath = path.resolve(__dirname, '../cli.js') 5 | const content = fs.readFileSync(filePath, 'utf8') 6 | const fixedContent = content.replace(/\r/g, '') 7 | fs.writeFileSync(filePath, fixedContent, 'utf8') 8 | -------------------------------------------------------------------------------- /test/tests/multi-platform.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import * as path from 'path' 3 | 4 | describe('Multi platform', () => { 5 | test('cli.js must not have \\r symbols', () => { 6 | const content = fs.readFileSync( 7 | path.resolve(__dirname, '../../cli.js'), 8 | 'utf8', 9 | ) 10 | 11 | expect(content).not.toContain('\r') 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | test 4 | scripts 5 | .git 6 | .gitignore 7 | index.ts 8 | tsconfig.json 9 | tslint.json 10 | webpack.config.js 11 | webpack.config.dev.js 12 | dist/coverage 13 | tmp 14 | .vscode 15 | cache 16 | .npmignore 17 | .prettierrc 18 | global.d.ts 19 | jest.config.js 20 | test/sources/default/index.d.ts 21 | test/sources/default/test.d.ts 22 | test/sources/js/index.d.ts 23 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const nodeModules = require('./scripts/nodeModules.js') 2 | 3 | module.exports = { 4 | transform: { 5 | '.(ts)$': 'ts-jest', 6 | }, 7 | modulePaths: nodeModules.find(__dirname), 8 | cacheDirectory: '/cache/jest', 9 | testRegex: '\\.test\\.ts$', 10 | moduleFileExtensions: ['js', 'json', 'ts'], 11 | fakeTimers: { 12 | enableGlobally: true, 13 | }, 14 | verbose: false, 15 | } 16 | -------------------------------------------------------------------------------- /test/sources/default/src/a.ts: -------------------------------------------------------------------------------- 1 | import * as winston from 'winston' 2 | import {B} from './b' 3 | import {IText} from './c' 4 | import {ISuggestedText} from './interfaces' 5 | 6 | export class A extends B { 7 | public static test(text: IText) { 8 | super.test(text) 9 | process.stdout.write(`\nA: ${text}\n`) 10 | } 11 | 12 | public static getText(): ISuggestedText { 13 | winston.warn('warn') 14 | return {value: 'test'} 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "lib": ["es2015", "es2017", "dom"], 5 | "sourceMap": true, 6 | "skipLibCheck": true, 7 | "noImplicitAny": true, 8 | "typeRoots": ["./node_modules/@types"] 9 | }, 10 | "include": ["./**/*.ts"], 11 | "exclude": [ 12 | "test", 13 | "**/*.test.ts", 14 | "node_modules", 15 | "bower_components", 16 | "cache", 17 | "build", 18 | "static" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": true, 5 | "target": "ES5", 6 | "moduleResolution": "node", 7 | "lib": ["es2015", "es2017", "dom"], 8 | "sourceMap": true, 9 | "skipLibCheck": true, 10 | "noImplicitAny": true, 11 | "noImplicitUseStrict": true, 12 | "typeRoots": ["./node_modules/@types"] 13 | }, 14 | "include": ["./**/*.ts", "./**/*.js"], 15 | "exclude": [ 16 | "**/*.test.ts", 17 | "jest.config.js", 18 | "cli.js", 19 | "docs", 20 | "dist", 21 | "node_modules", 22 | "bower_components", 23 | "cache", 24 | "build", 25 | "static", 26 | "scripts", 27 | "webpack.config.dev.js", 28 | "webpack.config.js" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "type": "npm", 9 | "script": "buildDev", 10 | "group": { 11 | "kind": "build", 12 | "isDefault": true 13 | }, 14 | "problemMatcher": [] 15 | }, 16 | { 17 | "type": "npm", 18 | "script": "docs", 19 | "problemMatcher": [] 20 | }, 21 | { 22 | "type": "npm", 23 | "script": "build", 24 | "problemMatcher": [] 25 | }, 26 | { 27 | "type": "npm", 28 | "script": "exec", 29 | "problemMatcher": [] 30 | }, 31 | { 32 | "type": "npm", 33 | "script": "testSync", 34 | "problemMatcher": [] 35 | }, 36 | { 37 | "type": "npm", 38 | "script": "lint", 39 | "problemMatcher": [] 40 | }, 41 | { 42 | "type": "npm", 43 | "script": "dts", 44 | "problemMatcher": [] 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /scripts/nodeModules.js: -------------------------------------------------------------------------------- 1 | const findNodeModules = require('find-node-modules') 2 | const fs = require('fs') 3 | const path = require('path') 4 | 5 | module.exports = { 6 | find: function(address, requiredModule) { 7 | let paths = findNodeModules(path.resolve(address)) 8 | 9 | if (requiredModule) { 10 | for (let i in paths) { 11 | let pathItem = path.resolve(address, paths[i]) 12 | 13 | if (fs.existsSync(path.resolve(pathItem, requiredModule))) { 14 | return pathItem 15 | } 16 | } 17 | } else { 18 | if (!paths) { 19 | throw new Error( 20 | 'Build scripts could not find location of node_modules.', 21 | ) 22 | } 23 | 24 | for (let i in paths) { 25 | paths[i] = path.resolve(address, paths[i]) 26 | } 27 | 28 | return paths 29 | } 30 | 31 | throw new Error( 32 | 'Could not locate node_modules containing module: ' + 33 | requiredModule.toString(), 34 | ) 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended"], 3 | "rules": { 4 | "indent": [true, "spaces", 2], 5 | "quotemark": [true, "single", "avoid-template"], 6 | "semicolon": [true, "never"], 7 | "no-empty-interface": false, 8 | "object-literal-sort-keys": false, 9 | "no-trailing-whitespace": [true, "ignore-blank-lines"], 10 | "arrow-parens": [true, "ban-single-arg-parens"], 11 | "completed-docs": [ 12 | true, 13 | { 14 | "classes": {"visibilities": ["exported"]}, 15 | "enums": {"visibilities": ["exported"]}, 16 | "enum-members": {"visibilities": ["exported"]}, 17 | "functions": {"visibilities": ["exported"]}, 18 | "interfaces": {"visibilities": ["exported"]}, 19 | "methods": {"locations": "all", "privacies": ["public", "protected"]}, 20 | "namespaces": {"visibilities": ["exported"]}, 21 | "properties": {"locations": "all", "privacies": ["public", "protected"]}, 22 | "types": {"visibilities": ["exported"]}, 23 | "variables": {"visibilities": ["exported"]} 24 | } 25 | ] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Vytenis Urbonavičius 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 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "args": [], 9 | "cwd": "${workspaceRoot}", 10 | "env": { 11 | "NODE_ENV": "development" 12 | }, 13 | "console": "integratedTerminal", 14 | "name": "DEBUG", 15 | "outFiles": [ "${workspaceRoot}/dist/**/*" ], 16 | "preLaunchTask": "build", 17 | "program": "${workspaceRoot}/index.ts", 18 | "request": "launch", 19 | "runtimeArgs": [ 20 | "--nolazy" 21 | ], 22 | "runtimeExecutable": null, 23 | "sourceMaps": true, 24 | "stopOnEntry": false, 25 | "type": "node" 26 | }, 27 | { 28 | "name": "Attach", 29 | "type": "node", 30 | "request": "attach", 31 | "port": 5858 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const nodeExternals = require('webpack-node-externals') 3 | const LicenseWebpackPlugin = require('license-webpack-plugin') 4 | .LicenseWebpackPlugin 5 | 6 | const exportedConfig = { 7 | entry: __dirname + '/index.ts', 8 | target: 'node', 9 | node: { 10 | __dirname: false, 11 | }, 12 | devtool: 'inline-source-map', 13 | externals: [nodeExternals()], 14 | plugins: [new LicenseWebpackPlugin()], 15 | mode: 'development', 16 | resolve: { 17 | extensions: ['.webpack.js', '.web.js', '.ts', '.js'], 18 | }, 19 | output: { 20 | path: __dirname + '/dist', 21 | filename: 'index.js', 22 | sourceMapFilename: 'index.js.map', 23 | libraryTarget: 'umd', 24 | }, 25 | resolveLoader: { 26 | modules: [__dirname + '/node_modules'], 27 | }, 28 | module: { 29 | rules: [ 30 | { 31 | test: /\.ts$/, 32 | use: [ 33 | { 34 | loader: 'ts-loader', 35 | options: { 36 | configFile: __dirname + '/tsconfig.json', 37 | }, 38 | }, 39 | ], 40 | }, 41 | ], 42 | }, 43 | } 44 | 45 | module.exports = exportedConfig 46 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const nodeExternals = require('webpack-node-externals') 3 | const LicenseWebpackPlugin = require('license-webpack-plugin') 4 | .LicenseWebpackPlugin 5 | 6 | const exportedConfig = { 7 | entry: __dirname + '/index.ts', 8 | target: 'node', 9 | node: { 10 | __dirname: false, 11 | }, 12 | devtool: 'source-map', 13 | optimization: { 14 | minimize: true, 15 | }, 16 | mode: 'production', 17 | externals: [nodeExternals()], 18 | plugins: [new LicenseWebpackPlugin()], 19 | resolve: { 20 | extensions: ['.webpack.js', '.web.js', '.ts', '.js'], 21 | }, 22 | output: { 23 | path: __dirname + '/dist', 24 | filename: 'index.js', 25 | sourceMapFilename: 'index.js.map', 26 | libraryTarget: 'umd', 27 | }, 28 | resolveLoader: { 29 | modules: [__dirname + '/node_modules'], 30 | }, 31 | module: { 32 | rules: [ 33 | { 34 | test: /\.ts$/, 35 | use: [ 36 | { 37 | loader: 'ts-loader', 38 | options: { 39 | configFile: __dirname + '/tsconfig.json', 40 | }, 41 | }, 42 | ], 43 | }, 44 | ], 45 | }, 46 | } 47 | 48 | module.exports = exportedConfig 49 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "todohighlight.keywords": [ 3 | { 4 | "text": "TODO:", 5 | "font-weight": "bold", 6 | "color": "#ffaa00", 7 | "border-bottom": "1px solid red", 8 | "borderRadius": "2px", //NOTE: using borderRadius along with `border` or you will see nothing change 9 | "backgroundColor": "rgba(0,0,0,0)" 10 | }, 11 | { 12 | "text": "FIXME:", 13 | "font-weight": "bold", 14 | "color": "#ff5522", 15 | "border-bottom": "1px solid red", 16 | "borderRadius": "2px", //NOTE: using borderRadius along with `border` or you will see nothing change 17 | "backgroundColor": "rgba(0,0,0,0)" 18 | } 19 | ], 20 | "todohighlight.isCaseSensitive": false, 21 | "javascript.implicitProjectConfig.checkJs": true, 22 | "editor.detectIndentation": false, 23 | "editor.formatOnSave": true, 24 | "editor.trimAutoWhitespace": false, 25 | "files.eol": "\n", 26 | "files.trimTrailingWhitespace": true, 27 | "files.insertFinalNewline": true, 28 | "debug.inlineValues": true, 29 | "html.format.indentInnerHtml": true, 30 | "html.format.maxPreserveNewLines": 1, 31 | "html.format.indentHandlebars": true, 32 | "html.format.extraLiners": "", 33 | "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": true, 34 | "coverage-gutters.showLineCoverage": true, 35 | "coverage-gutters.lcovname": "dist/coverage/lcov.info", 36 | "coverage-gutters.showRulerCoverage": true, 37 | "coverage-gutters.showGutterCoverage": true, 38 | "tslint.ignoreDefinitionFiles": false, 39 | "tslint.autoFixOnSave": true, 40 | "tslint.alwaysShowRuleFailuresAsWarnings": true, 41 | "tslint.exclude": "**/node_modules/**/*", 42 | "editor.wrappingIndent": "indent", 43 | "cSpell.ignoreRegExpList": ["/E[A-Z][A-Z]+/"], 44 | "typescript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": false, 45 | "cSpell.words": [ 46 | "MAVGS", 47 | "USDJPY", 48 | "bitbucket", 49 | "devtool", 50 | "printf", 51 | "truthy", 52 | "typedoc", 53 | "urbonavicius", 54 | "urbonavičius", 55 | "vytenis", 56 | "vytenisu", 57 | "whitend" 58 | ], 59 | "editor.tabSize": 2, 60 | "prettier.useTabs": false, 61 | "editor.useTabStops": false, 62 | "editor.insertSpaces": true 63 | } 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "npm-dts", 3 | "version": "1.3.13", 4 | "description": "Simple DTS single-file generation utility for TypeScript bundles", 5 | "main": "dist/index.js", 6 | "bin": { 7 | "npm-dts": "cli.js" 8 | }, 9 | "scripts": { 10 | "prepublishOnly": "npm run lint && npm run build && npm run fix && npm run test && npm run docs && npm run dts", 11 | "test": "jest --forceExit --detectOpenHandles", 12 | "testSync": "jest --runInBand --forceExit --detectOpenHandles", 13 | "lint": "tslint -c ./tslint.json -p ./tsconfig.json -t stylish ./lib/**/*.ts", 14 | "build": "webpack", 15 | "buildDev": "webpack --config webpack.config.dev.js", 16 | "docs": "typedoc --out ./docs --readme ./README.md --exclude **/*.test.* --excludePrivate --excludeProtected --excludeExternals --darkHighlightTheme dark-plus --hideGenerator ./index.ts", 17 | "dts": "npm run exec", 18 | "exec": "node ./cli.js generate", 19 | "fix": "node ./scripts/fixCliLineEndings.js" 20 | }, 21 | "homepage": "https://github.com/vytenisu/npm-dts", 22 | "bugs": { 23 | "url": "https://github.com/vytenisu/npm-dts/issues" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/vytenisu/npm-dts.git" 28 | }, 29 | "keywords": [ 30 | "NPM", 31 | "dts", 32 | "cli", 33 | "package", 34 | "ts", 35 | "TypeScript", 36 | "tsc", 37 | "index.d.ts", 38 | "single", 39 | "file", 40 | "bundle", 41 | "concatenate", 42 | "simple", 43 | "auto", 44 | "generator", 45 | "vytenis", 46 | "urbonavicius", 47 | "vytenisu", 48 | "WhiteTurbine", 49 | "Whitend" 50 | ], 51 | "author": { 52 | "name": "Vytenis Urbonavičius", 53 | "url": "https://github.com/vytenisu" 54 | }, 55 | "license": "MIT", 56 | "devDependencies": { 57 | "@types/args": "5.0.3", 58 | "@types/jest": "29.5.12", 59 | "@types/mkdirp": "2.0.0", 60 | "@types/rimraf": "4.0.5", 61 | "@types/tmp": "0.2.6", 62 | "@types/winston": "2.4.4", 63 | "jest": "29.7.0", 64 | "license-webpack-plugin": "4.0.2", 65 | "ts-jest": "29.2.4", 66 | "ts-loader": "9.5.1", 67 | "ts-node": "10.9.2", 68 | "tslint": "6.1.3", 69 | "typedoc": "0.22.17", 70 | "typescript": "4.7.4", 71 | "webpack": "5.93.0", 72 | "webpack-cli": "5.1.4", 73 | "webpack-node-externals": "3.0.0" 74 | }, 75 | "dependencies": { 76 | "args": "5.0.3", 77 | "find-node-modules": "2.1.3", 78 | "mkdirp": "3.0.1", 79 | "npm-run": "5.0.1", 80 | "rimraf": "6.0.1", 81 | "tmp": "0.2.3", 82 | "winston": "3.13.1" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/log.ts: -------------------------------------------------------------------------------- 1 | import * as winston from 'winston' 2 | import { 3 | debug as winstonDebug, 4 | error as winstonError, 5 | info as winstonInfo, 6 | silly as winstonSilly, 7 | verbose as winstonVerbose, 8 | warn as winstonWarn, 9 | } from 'winston' 10 | 11 | winston.addColors({ 12 | error: 'red', 13 | warn: 'yellow', 14 | info: 'cyan', 15 | debug: 'green', 16 | }) 17 | 18 | let logEnabled = false 19 | 20 | /** 21 | * Supported debug levels 22 | */ 23 | export enum ELogLevel { 24 | /** 25 | * Error 26 | */ 27 | error = 'error', 28 | 29 | /** 30 | * Warning 31 | */ 32 | warn = 'warn', 33 | 34 | /** 35 | * Information 36 | */ 37 | info = 'info', 38 | 39 | /** 40 | * Verbose information 41 | */ 42 | verbose = 'verbose', 43 | 44 | /** 45 | * Debug information 46 | */ 47 | debug = 'debug', 48 | } 49 | 50 | /** 51 | * Logs error message 52 | * @param message Message to be logged 53 | */ 54 | export const error = (message: string) => { 55 | if (logEnabled) { 56 | return winstonError(message) 57 | } else { 58 | return null 59 | } 60 | } 61 | 62 | /** 63 | * Logs warning message 64 | * @param message Message to be logged 65 | */ 66 | export const warn = (message: string) => { 67 | if (logEnabled) { 68 | return winstonWarn(message) 69 | } else { 70 | return null 71 | } 72 | } 73 | 74 | /** 75 | * Logs informational message 76 | * @param message Message to be logged 77 | */ 78 | export const info = (message: string) => { 79 | if (logEnabled) { 80 | return winstonInfo(message) 81 | } else { 82 | return null 83 | } 84 | } 85 | 86 | /** 87 | * Logs verbose message 88 | * @param message Message to be logged 89 | */ 90 | export const verbose = (message: string) => { 91 | if (logEnabled) { 92 | return winstonVerbose(message) 93 | } else { 94 | return null 95 | } 96 | } 97 | 98 | /** 99 | * Logs debug message 100 | * @param message Message to be logged 101 | */ 102 | export const debug = (message: string) => { 103 | if (logEnabled) { 104 | return winstonDebug(message) 105 | } else { 106 | return null 107 | } 108 | } 109 | 110 | /** 111 | * Initializes and enables logging 112 | * @param label prefix to be used before each log line 113 | */ 114 | export const init = (label: string, level: ELogLevel) => { 115 | winston.configure({ 116 | level, 117 | format: winston.format.combine( 118 | winston.format.colorize(), 119 | winston.format.label({label}), 120 | winston.format.timestamp(), 121 | winston.format.prettyPrint(), 122 | winston.format.printf( 123 | (parts: any) => `[${parts.label}] [${parts.level}] : ${parts.message}`, 124 | ), 125 | ), 126 | transports: [new winston.transports.Console()], 127 | }) 128 | 129 | logEnabled = true 130 | } 131 | -------------------------------------------------------------------------------- /test/tests/e2e.test.ts: -------------------------------------------------------------------------------- 1 | import {execSync as exec} from 'child_process' 2 | import {readFileSync, unlinkSync} from 'fs' 3 | import * as path from 'path' 4 | 5 | describe('Default behavior', () => { 6 | const scriptPath = path.resolve(__dirname, '..', '..', 'cli.js') 7 | const projectPath = path.resolve(__dirname, '..', 'sources', 'default') 8 | const jsProjectPath = path.resolve(__dirname, '..', 'sources', 'js') 9 | const customOutput = 'test.d.ts' 10 | 11 | const dtsPath = path.resolve( 12 | __dirname, 13 | '..', 14 | 'sources', 15 | 'default', 16 | 'index.d.ts', 17 | ) 18 | 19 | const customDtsPath = path.resolve( 20 | __dirname, 21 | '..', 22 | 'sources', 23 | 'default', 24 | customOutput, 25 | ) 26 | 27 | const jsDtsPath = path.resolve(__dirname, '..', 'sources', 'js', 'index.d.ts') 28 | 29 | let source: string 30 | let customDtsSource: string 31 | let jsSource: string 32 | 33 | beforeAll(() => { 34 | try { 35 | unlinkSync(dtsPath) 36 | } catch (e) { 37 | // NOT NEEDED 38 | } 39 | 40 | exec( 41 | `node "${scriptPath}" -m -r "${projectPath}" -c " -p tsconfig.test.json" generate`, 42 | ) 43 | 44 | exec( 45 | `node "${scriptPath}" -m -r "${projectPath}" -c " -p tsconfig.test.json" -o ${customOutput} generate`, 46 | ) 47 | 48 | exec( 49 | `node "${scriptPath}" -m -r "${jsProjectPath}" -c " -p tsconfig.test.json" -e index.js generate`, 50 | ) 51 | 52 | source = readFileSync(dtsPath, {encoding: 'utf8'}) 53 | customDtsSource = readFileSync(customDtsPath, {encoding: 'utf8'}) 54 | jsSource = readFileSync(jsDtsPath, {encoding: 'utf8'}) 55 | }) 56 | 57 | afterAll(() => { 58 | unlinkSync(dtsPath) 59 | unlinkSync(customDtsPath) 60 | unlinkSync(jsDtsPath) 61 | }) 62 | 63 | it('exports all TS classes', () => { 64 | const classes = ['A', 'B', 'C'] 65 | 66 | classes.forEach(cls => { 67 | expect(source.includes(`export class ${cls}`)).toBeTruthy() 68 | }) 69 | }) 70 | 71 | it('exports all JS classes', () => { 72 | const classes = ['XXX', 'YYY'] 73 | 74 | classes.forEach(cls => { 75 | expect(jsSource.includes(`export class ${cls}`)).toBeTruthy() 76 | }) 77 | }) 78 | 79 | it('exports all types', () => { 80 | const interfaces = ['IText'] 81 | 82 | interfaces.forEach(int => { 83 | expect(source.includes(`export type ${int}`)).toBeTruthy() 84 | }) 85 | }) 86 | 87 | it('exports all interfaces', () => { 88 | const interfaces = ['ISuggestedText', 'ASchema'] 89 | 90 | interfaces.forEach(int => { 91 | expect(source.includes(`export interface ${int}`)).toBeTruthy() 92 | }) 93 | }) 94 | 95 | it('does not leave relative paths', () => { 96 | expect(source.includes("from '.")).toBeFalsy() 97 | expect(jsSource.includes("from '.")).toBeFalsy() 98 | expect(source.includes("import('.")).toBeFalsy() 99 | expect(jsSource.includes("import('.")).toBeFalsy() 100 | }) 101 | 102 | it('does not touch 3rd party module imports', () => { 103 | expect(source.includes("'winston'")).toBeTruthy() 104 | }) 105 | 106 | it('works correctly when index.ts is used', () => { 107 | expect( 108 | source.includes("from 'test-default/test/sources/default/src/c/index'"), 109 | ).toBeTruthy() 110 | expect( 111 | source.includes( 112 | "declare module 'test-default/test/sources/default/src/c/index'", 113 | ), 114 | ).toBeTruthy() 115 | }) 116 | 117 | it('works correctly when index.ts is not used', () => { 118 | const modules = ['A', 'B'] 119 | 120 | modules.forEach(m => { 121 | expect( 122 | source.includes( 123 | `declare module 'test-default/test/sources/default/src/${m.toLowerCase()}'`, 124 | ), 125 | ).toBeTruthy() 126 | 127 | expect( 128 | source.includes( 129 | `from 'test-default/test/sources/default/src/${m.toLowerCase()}'`, 130 | ), 131 | ).toBeTruthy() 132 | }) 133 | }) 134 | 135 | it('works correctly when module has a dot in its name', () => { 136 | expect( 137 | source.includes( 138 | "declare module 'test-default/test/sources/default/src/a.schema'", 139 | ), 140 | ).toBeTruthy() 141 | 142 | expect( 143 | source.includes("from 'test-default/test/sources/default/src/a.schema'"), 144 | ).toBeTruthy() 145 | }) 146 | 147 | it('exports main NPM package module', () => { 148 | expect(source.includes("declare module 'test-default'")).toBeTruthy() 149 | expect(jsSource.includes("declare module 'test-js'")).toBeTruthy() 150 | }) 151 | 152 | it('exports entry point under module name', () => { 153 | expect(source.includes("require('test-default/index')")).toBeTruthy() 154 | expect(jsSource.includes("require('test-js/index')")).toBeTruthy() 155 | }) 156 | 157 | it('re-exports JS modules', () => { 158 | const modules = ['XXX', 'YYY'] 159 | modules.forEach(m => { 160 | expect(jsSource.includes(`export const ${m}`)).toBeTruthy() 161 | }) 162 | }) 163 | 164 | it('allows to customize output target', () => { 165 | expect(source).toBe(customDtsSource) 166 | }) 167 | }) 168 | -------------------------------------------------------------------------------- /lib/cli.ts: -------------------------------------------------------------------------------- 1 | import * as args from 'args' 2 | import * as path from 'path' 3 | import {ELogLevel} from './log' 4 | 5 | /** 6 | * CLI argument names 7 | */ 8 | export enum ECliArgument { 9 | /** 10 | * Main file of non-bundled package source 11 | */ 12 | entry = 'entry', 13 | 14 | /** 15 | * Root directory of targeted package 16 | */ 17 | root = 'root', 18 | 19 | /** 20 | * Temporary directory required during generation 21 | */ 22 | tmp = 'tmp', 23 | 24 | /** 25 | * Additional TSC properties 26 | */ 27 | tsc = 'tsc', 28 | 29 | /** 30 | * Selected logging level 31 | */ 32 | logLevel = 'logLevel', 33 | 34 | /** 35 | * Output file path (relative to root) 36 | */ 37 | output = 'output', 38 | 39 | /** 40 | * Flag which forces using own TSC as opposed to target TSC 41 | * This should only be used for testing npm-dts itself 42 | * This is because it generates incorrect module names 43 | */ 44 | testMode = 'testMode', 45 | 46 | /** 47 | * Flag which forces attempting generation at least partially despite errors 48 | */ 49 | force = 'force', 50 | } 51 | 52 | /** 53 | * Configuration structure for generating an aggregated dts file 54 | */ 55 | export interface INpmDtsArgs { 56 | /** 57 | * Iterator 58 | */ 59 | [argName: string]: string | boolean 60 | 61 | /** 62 | * Main file of non-bundled package source. Can be a path relative to TSC rootDir. 63 | */ 64 | entry?: string 65 | 66 | /** 67 | * Root directory of targeted package 68 | */ 69 | root?: string 70 | 71 | /** 72 | * Temporary directory required during generation 73 | */ 74 | tmp?: string 75 | 76 | /** 77 | * Additional TSC properties 78 | */ 79 | tsc?: string 80 | 81 | /** 82 | * Selected logging level 83 | */ 84 | logLevel?: ELogLevel 85 | 86 | /** 87 | * Attempts to at least partially generate typings ignoring non-critical errors 88 | */ 89 | force?: boolean 90 | 91 | /** 92 | * Output file path (relative to root) 93 | */ 94 | output?: string 95 | 96 | /** 97 | * Flag which forces using own TSC as opposed to target TSC 98 | * This should only be used for testing npm-dts itself 99 | * This is because it generates incorrect module names 100 | */ 101 | testMode?: boolean 102 | } 103 | 104 | /** 105 | * CLI usage logic 106 | */ 107 | export class Cli { 108 | /** 109 | * Stores whether module was successfully launched 110 | */ 111 | protected launched = false 112 | 113 | /** 114 | * Stores whether TMP directory location was passed 115 | */ 116 | protected tmpPassed = false 117 | 118 | /** 119 | * Stores current CLI argument values 120 | */ 121 | private args: INpmDtsArgs = { 122 | entry: 'index.ts', 123 | root: path.resolve(process.cwd()), 124 | tmp: '', 125 | tsc: '', 126 | logLevel: ELogLevel.info, 127 | force: false, 128 | output: 'index.d.ts', 129 | testMode: false, 130 | } 131 | 132 | /** 133 | * Automatically reads CLI arguments and performs actions based on them 134 | */ 135 | public constructor(injectedArguments?: INpmDtsArgs) { 136 | if (injectedArguments) { 137 | this.launched = true 138 | this.storeArguments(injectedArguments) 139 | } else { 140 | args 141 | .option( 142 | ['e', 'entry'], 143 | 'Entry/main package file before bundling, relative to project root', 144 | ) 145 | .option( 146 | ['r', 'root'], 147 | 'NPM package directory containing package.json', 148 | this.args.root, 149 | ) 150 | .option( 151 | ['t', 'tmp'], 152 | 'Directory for storing temporary information', 153 | this.args.tmp, 154 | (value: string) => { 155 | if (!value.includes('<')) { 156 | this.tmpPassed = true 157 | } 158 | 159 | return value 160 | }, 161 | ) 162 | .option( 163 | ['c', 'tsc'], 164 | 'Passed through non-validated additional TSC options', 165 | this.args.tsc, 166 | ) 167 | .option( 168 | ['L', 'logLevel'], 169 | 'Log level (error, warn, info, verbose, debug)', 170 | this.args.logLevel, 171 | ) 172 | .option( 173 | ['f', 'force'], 174 | 'Ignores non-critical errors and attempts to at least partially generate typings', 175 | this.args.force, 176 | ) 177 | .option( 178 | ['o', 'output'], 179 | 'Overrides recommended output target to a custom one', 180 | this.args.output, 181 | ) 182 | .option( 183 | ['m', 'testMode'], 184 | 'Configures npm-dts for self-test', 185 | this.args.testMode, 186 | ) 187 | .command('generate', 'Start generation', (name, sub, options) => { 188 | this.launched = true 189 | this.storeArguments(options) 190 | }) 191 | .example( 192 | 'npm-dts generate', 193 | 'Generates index.d.ts file and updates package.json for CWD.', 194 | ) 195 | .example( 196 | 'npm-dts -r /your/project/path generate', 197 | 'Performs generation on a custom path.', 198 | ) 199 | 200 | args.parse(process.argv, { 201 | name: 'npm-dts', 202 | mri: {}, 203 | mainColor: 'yellow', 204 | subColor: 'dim', 205 | }) 206 | 207 | if (!this.launched) { 208 | args.showHelp() 209 | } 210 | } 211 | } 212 | 213 | /** 214 | * Gathers current value of a particular CLI argument 215 | * @param arg argument name 216 | */ 217 | protected getArgument(arg: ECliArgument) { 218 | return this.args[arg] 219 | } 220 | 221 | /** 222 | * Dynamically overrides value of stored argument 223 | * @param arg argument name 224 | * @param value argument value 225 | */ 226 | protected setArgument(arg: ECliArgument, value: string | boolean) { 227 | // @ts-ignore 228 | this.args[arg] = value 229 | } 230 | 231 | /** 232 | * Stores entered CLI arguments 233 | * @param passedArguments arguments entered to CLI 234 | */ 235 | private storeArguments(passedArguments: any = this.args) { 236 | for (const argName of Object.keys(this.args)) { 237 | this.args[argName] = Object.is(passedArguments[argName], undefined) 238 | ? this.args[argName] 239 | : passedArguments[argName] 240 | } 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # npm-dts 2 | 3 | _by Vytenis Urbonavičius_ 4 | 5 | This utility generates single _index.d.ts_ file for whole NPM package. 6 | 7 | It allows creating bundled _NPM_ library packages without _TypeScript_ sources and yet still keeping code suggestions wherever these libraries are imported. 8 | 9 | _TypeScript_ picks up _index.d.ts_ automatically. 10 | 11 | --- 12 | 13 | **Some useful forks:** 14 | 15 | * **[@htho/npm-dts](https://www.npmjs.com/package/@htho/npm-dts) by [Hauke Thorenz](https://www.npmjs.com/~htho)** - custom alias functionality (--customAlias), tree shaking (--shake) 16 | 17 | --- 18 | 19 | ## Installation 20 | 21 | Local: 22 | 23 | ``` 24 | npm install --save-dev npm-dts 25 | ``` 26 | 27 | Global: 28 | 29 | ``` 30 | npm install -g npm-dts 31 | ``` 32 | 33 | --- 34 | 35 | ## CLI Usage 36 | 37 | Please make sure that target project has _"typescript"_ installed in _node_modules_. 38 | 39 | To see full _CLI_ help - run without arguments: 40 | 41 | ``` 42 | npm-dts 43 | ``` 44 | 45 | Typical usage (using global install): 46 | 47 | ``` 48 | cd /your/project 49 | npm-dts generate 50 | ``` 51 | 52 |
53 | 54 | ### Supported options 55 | 56 | ``` 57 | npm-dts [options] generate 58 | ``` 59 | 60 | | Option                              | Alias                             | Description | 61 | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 62 | | --entry [file] | -e [file] | Allows changing main _src_ file from _index.ts_ to something else. It can also be declared as a path, relative to _rootDir_ of _TSC_. Note that if _rootDir_ is not specified in _tsconfig.json_ and all _TS_ source code is under some sub-directory such as "src" - _TSC_ might auto-magically set _rootDir_ to "src". | 63 | | --force | -f | Ignores non-critical errors and attempts to at least partially generate typings (disabled by default). | 64 | | --help | -h | Output usage information. | 65 | | --logLevel [level] | -L [level] | Log level (error, warn, info, verbose, debug) (defaults to "info"). | 66 | | --output [file] | -o [file] | Overrides recommended output target to a custom one (defaults to "index.d.ts"). | 67 | | --root [path] | -r [path] | NPM package directory containing package.json (defaults to current working directory). | 68 | | --tmp [path] | -t [path] | Directory for storing temporary information (defaults to OS-specific temporary directory). Note that tool completely deletes this folder once finished. | 69 | | --tsc [options] | -c [options] | Passed through additional TSC options (defaults to ""). Note that they are not validated or checked for suitability. When passing through CLI it is recommended to surround arguments in quotes **and start with a space** (work-around for a bug in argument parsing dependency of _npm-dts_). | 70 | | --version | -v | Output the version number. | 71 | 72 |
73 | 74 | ## Integration using _WebPack_ 75 | 76 | You would want to use [**"npm-dts-webpack-plugin"**](https://www.npmjs.com/package/npm-dts-webpack-plugin) package instead. 77 | 78 |
79 | 80 | ## Integration into _NPM_ scripts 81 | 82 | Example of how you could run generation of _index.d.ts_ automatically before every publish. 83 | 84 | ``` 85 | { 86 | // ...... 87 | "scripts": { 88 | "prepublishOnly": "npm run dts && ......", 89 | "dts": "./node_modules/.bin/npm-dts generate" 90 | } 91 | // ...... 92 | } 93 | ``` 94 | 95 | Another possible option would be to execute "npm run dts" as part of bundling task. 96 | 97 |
98 | 99 | ## Integration into custom solution 100 | 101 | This approach can be used for integration with tools such as _WebPack_. 102 | 103 | Simple usage with all default values: 104 | 105 | ```typescript 106 | import {Generator} from 'npm-dts' 107 | new Generator({}).generate() 108 | ``` 109 | 110 | Advanced usage example with some arguments overridden: 111 | 112 | ```typescript 113 | import * as path from 'path' 114 | import {Generator} from 'npm-dts' 115 | 116 | new Generator({ 117 | entry: 'main.ts', 118 | root: path.resolve(process.cwd(), 'project'), 119 | tmp: path.resolve(process.cwd(), 'cache/tmp'), 120 | tsc: '--extendedDiagnostics', 121 | }).generate() 122 | ``` 123 | 124 | Above examples were in _TypeScript_. Same in plain _JavaScript_ would look like this: 125 | 126 | ```javascript 127 | const path = require('path') 128 | 129 | new (require('npm-dts').Generator)({ 130 | entry: 'main.ts', 131 | root: path.resolve(process.cwd(), 'project'), 132 | tmp: path.resolve(process.cwd(), 'cache/tmp'), 133 | tsc: '--extendedDiagnostics', 134 | }).generate() 135 | ``` 136 | 137 | ### Additional arguments 138 | 139 | Constructor of generator also supports two more boolean flags as optional arguments: 140 | 141 | - Enable log 142 | - Throw exception on error 143 | 144 | Initializing without any options will cause _npm-cli_ to read CLI arguments all by itself. 145 | -------------------------------------------------------------------------------- /lib/generator.ts: -------------------------------------------------------------------------------- 1 | import {readdirSync, statSync, writeFileSync} from 'fs' 2 | import {readFileSync} from 'fs' 3 | import {mkdirp as mkdir} from 'mkdirp' 4 | import * as npmRun from 'npm-run' 5 | import {join, relative, resolve, dirname} from 'path' 6 | import {rimraf as rm} from 'rimraf' 7 | import * as tmp from 'tmp' 8 | import {Cli, ECliArgument, INpmDtsArgs} from './cli' 9 | import {debug, ELogLevel, error, info, init, verbose, warn} from './log' 10 | import * as fs from 'fs' 11 | 12 | const MKDIR_RETRIES = 5 13 | 14 | /** 15 | * Logic for generating aggregated typings for NPM module 16 | */ 17 | export class Generator extends Cli { 18 | private packageInfo: any 19 | private moduleNames: string[] 20 | private throwErrors: boolean 21 | private cacheContentEmptied: boolean = true 22 | 23 | /** 24 | * Auto-launches generation based on command line arguments 25 | * @param injectedArguments generation arguments (same as CLI) 26 | * @param enableLog enables logging when true, null allows application to decide 27 | * @param throwErrors makes generation throw errors when true 28 | */ 29 | public constructor( 30 | injectedArguments?: INpmDtsArgs, 31 | enableLog: boolean | null = null, 32 | throwErrors = false, 33 | ) { 34 | super(injectedArguments) 35 | 36 | this.throwErrors = throwErrors 37 | 38 | if (enableLog === null) { 39 | enableLog = !injectedArguments 40 | } 41 | 42 | if (enableLog) { 43 | init('npm-dts', this.getLogLevel()) 44 | 45 | const myPackageJson = JSON.parse( 46 | readFileSync(resolve(__dirname, '..', 'package.json'), { 47 | encoding: 'utf8', 48 | }), 49 | ) 50 | 51 | const soft = ` npm-dts v${myPackageJson.version} ` 52 | let author = ' by Vytenis Urbonavičius ' 53 | let spaces = ' ' 54 | let border = '___________________________________________________________' 55 | 56 | author = author.substring(0, soft.length) 57 | spaces = spaces.substring(0, soft.length) 58 | border = border.substring(0, soft.length) 59 | 60 | info(` ${border} `) 61 | info(`|${spaces}|`) 62 | info(`|${spaces}|`) 63 | info(`|${soft}|`) 64 | info(`|${author}|`) 65 | info(`|${spaces}|`) 66 | info(`|${border}|`) 67 | info(` ${spaces} `) 68 | } 69 | } 70 | 71 | /** 72 | * Executes generation of an aggregated dts file 73 | */ 74 | public async generate() { 75 | info(`Generating declarations for "${this.getRoot()}"...`) 76 | 77 | let hasError = false 78 | let exception = null 79 | const cleanupTasks: (() => void)[] = [] 80 | 81 | if (!this.tmpPassed) { 82 | verbose('Locating OS Temporary Directory...') 83 | 84 | try { 85 | await new Promise(done => { 86 | tmp.dir((tmpErr, tmpDir, rmTmp) => { 87 | if (tmpErr) { 88 | error('Could not create OS Temporary Directory!') 89 | this.showDebugError(tmpErr) 90 | throw tmpErr 91 | } 92 | 93 | verbose('OS Temporary Directory was located!') 94 | this.setArgument(ECliArgument.tmp, resolve(tmpDir, 'npm-dts')) 95 | 96 | cleanupTasks.push(() => { 97 | verbose('Deleting OS Temporary Directory...') 98 | rmTmp() 99 | verbose('OS Temporary Directory was deleted!') 100 | }) 101 | done() 102 | }) 103 | }) 104 | } catch (e) { 105 | hasError = true 106 | exception = e 107 | } 108 | } 109 | 110 | if (!hasError) { 111 | await this._generate().catch(async e => { 112 | hasError = true 113 | 114 | const output = this.getOutput() 115 | 116 | error(`Generation of ${output} has failed!`) 117 | this.showDebugError(e) 118 | 119 | if (!this.useForce()) { 120 | if (this.getLogLevel() === ELogLevel.debug) { 121 | info( 122 | 'If issue is not severe, you can try forcing execution using force flag.', 123 | ) 124 | info( 125 | 'In case of command line usage, add "-f" as the first parameter.', 126 | ) 127 | } else { 128 | info('You should try running npm-dts with debug level logging.') 129 | info( 130 | 'In case of command line, debug mode is enabled using "-L debug".', 131 | ) 132 | } 133 | } 134 | 135 | if (!this.cacheContentEmptied) { 136 | await this.clearTempDir() 137 | } 138 | 139 | exception = e 140 | }) 141 | } 142 | 143 | cleanupTasks.forEach(task => task()) 144 | 145 | if (!hasError) { 146 | info('Generation is completed!') 147 | } else { 148 | error('Generation failed!') 149 | 150 | if (this.throwErrors) { 151 | throw exception || new Error('Generation failed!') 152 | } 153 | } 154 | } 155 | 156 | /** 157 | * Logs serialized error if it exists 158 | * @param e - error to be shown 159 | */ 160 | private showDebugError(e: any) { 161 | if (e) { 162 | if (e.stdout) { 163 | debug(`Error: \n${e.stdout.toString()}`) 164 | } else { 165 | debug(`Error: \n${JSON.stringify(e)}`) 166 | } 167 | } 168 | } 169 | 170 | /** 171 | * Launches generation of typings 172 | */ 173 | private async _generate() { 174 | await this.generateTypings() 175 | let source = await this.combineTypings() 176 | source = this.addAlias(source) 177 | await this.storeResult(source) 178 | } 179 | 180 | private getLogLevel(): ELogLevel { 181 | const logLevel = this.getArgument(ECliArgument.logLevel) as ELogLevel 182 | return ELogLevel[logLevel] ? logLevel : ELogLevel.info 183 | } 184 | 185 | /** 186 | * Gathers entry file address (relative to project root path) 187 | */ 188 | private getEntry(): string { 189 | return this.getArgument(ECliArgument.entry) as string 190 | } 191 | 192 | /** 193 | * Gathers target project root path 194 | */ 195 | private getRoot(): string { 196 | return resolve(this.getArgument(ECliArgument.root) as string) 197 | } 198 | 199 | /** 200 | * Gathers TMP directory to be used for TSC operations 201 | */ 202 | private getTempDir(): string { 203 | return resolve(this.getArgument(ECliArgument.tmp) as string) 204 | } 205 | 206 | /** 207 | * Gathers output path to be used (relative to root) 208 | */ 209 | private getOutput(): string { 210 | return this.getArgument(ECliArgument.output) as string 211 | } 212 | 213 | /** 214 | * Checks if script is forced to use its built-in TSC 215 | */ 216 | private useTestMode(): boolean { 217 | return this.getArgument(ECliArgument.testMode) as boolean 218 | } 219 | 220 | /** 221 | * Checks if script is forced to attempt generation despite errors 222 | */ 223 | private useForce(): boolean { 224 | return this.getArgument(ECliArgument.force) as boolean 225 | } 226 | 227 | /** 228 | * Creates TMP directory to be used for TSC operations 229 | * @param retries amount of times to retry on failure 230 | */ 231 | private makeTempDir(retries = MKDIR_RETRIES): Promise { 232 | const tmpDir = this.getTempDir() 233 | verbose('Preparing "tmp" directory...') 234 | 235 | return new Promise((done, fail) => { 236 | mkdir(tmpDir) 237 | .then(() => { 238 | this.cacheContentEmptied = false 239 | verbose('"tmp" directory was prepared!') 240 | done() 241 | }) 242 | .catch(mkdirError => { 243 | error(`Failed to create "${tmpDir}"!`) 244 | this.showDebugError(mkdirError) 245 | 246 | if (retries) { 247 | const sleepTime = 100 248 | verbose(`Will retry in ${sleepTime}ms...`) 249 | 250 | setTimeout(() => { 251 | this.makeTempDir(retries - 1).then(done, fail) 252 | }, sleepTime) 253 | } else { 254 | error(`Stopped trying after ${MKDIR_RETRIES} retries!`) 255 | fail() 256 | } 257 | }) 258 | }) 259 | } 260 | 261 | /** 262 | * Removes TMP directory 263 | */ 264 | private clearTempDir() { 265 | const tmpDir = this.getTempDir() 266 | verbose('Cleaning up "tmp" directory...') 267 | 268 | return new Promise((done, fail) => { 269 | rm(tmpDir) 270 | .then(() => { 271 | this.cacheContentEmptied = true 272 | verbose('"tmp" directory was cleaned!') 273 | done() 274 | }) 275 | .catch(rmError => { 276 | error(`Could not clean up "tmp" directory at "${tmpDir}"!`) 277 | this.showDebugError(rmError) 278 | fail() 279 | }) 280 | }) 281 | } 282 | 283 | /** 284 | * Re-creates empty TMP directory to be used for TSC operations 285 | */ 286 | private resetCacheDir() { 287 | verbose('Will now reset "tmp" directory...') 288 | return new Promise((done, fail) => { 289 | this.clearTempDir().then(() => { 290 | this.makeTempDir().then(done, fail) 291 | }, fail) 292 | }) 293 | } 294 | 295 | /** 296 | * Generates per-file typings using TSC 297 | */ 298 | private async generateTypings() { 299 | await this.resetCacheDir() 300 | 301 | verbose('Generating per-file typings using TSC...') 302 | 303 | const tscOptions = this.getArgument(ECliArgument.tsc) as string 304 | 305 | const cmd = 306 | 'tsc --declaration --emitDeclarationOnly --declarationDir "' + 307 | this.getTempDir() + 308 | '"' + 309 | (tscOptions.length ? ` ${tscOptions}` : '') 310 | 311 | debug(cmd) 312 | 313 | try { 314 | npmRun.execSync( 315 | cmd, 316 | { 317 | cwd: this.useTestMode() ? resolve(__dirname, '..') : this.getRoot(), 318 | }, 319 | (err: any, stdout: any, stderr: any) => { 320 | if (err) { 321 | if (this.useForce()) { 322 | warn('TSC exited with errors!') 323 | } else { 324 | error('TSC exited with errors!') 325 | } 326 | 327 | this.showDebugError(err) 328 | } else { 329 | if (stdout) { 330 | process.stdout.write(stdout) 331 | } 332 | 333 | if (stderr) { 334 | process.stderr.write(stderr) 335 | } 336 | } 337 | }, 338 | ) 339 | } catch (e) { 340 | if (this.useForce()) { 341 | warn('Suppressing errors due to "force" flag!') 342 | this.showDebugError(e) 343 | warn('Generated declaration files might not be valid!') 344 | } else { 345 | throw e 346 | } 347 | } 348 | 349 | verbose('Per-file typings have been generated using TSC!') 350 | } 351 | 352 | /** 353 | * Gathers a list of created per-file declaration files 354 | * @param dir directory to be scanned for files (called during recursion) 355 | * @param files discovered array of files (called during recursion) 356 | */ 357 | private getDeclarationFiles( 358 | dir: string = this.getTempDir(), 359 | files: string[] = [], 360 | ) { 361 | if (dir === this.getTempDir()) { 362 | verbose('Loading list of generated typing files...') 363 | } 364 | 365 | try { 366 | readdirSync(dir).forEach(file => { 367 | if (statSync(join(dir, file)).isDirectory()) { 368 | files = this.getDeclarationFiles(join(dir, file), files) 369 | } else { 370 | files = files.concat(join(dir, file)) 371 | } 372 | }) 373 | } catch (e) { 374 | error('Failed to load list of generated typing files...') 375 | this.showDebugError(e) 376 | throw e 377 | } 378 | 379 | if (dir === this.getTempDir()) { 380 | verbose('Successfully loaded list of generated typing files!') 381 | } 382 | 383 | return files 384 | } 385 | 386 | /** 387 | * Loads package.json information of target project 388 | */ 389 | private getPackageDetails() { 390 | if (this.packageInfo) { 391 | return this.packageInfo 392 | } 393 | 394 | verbose('Loading package.json...') 395 | 396 | const root = this.getRoot() 397 | const packageJsonPath = resolve(root, 'package.json') 398 | 399 | try { 400 | this.packageInfo = JSON.parse( 401 | readFileSync(packageJsonPath, {encoding: 'utf8'}), 402 | ) 403 | } catch (e) { 404 | error(`Failed to read package.json at "'${packageJsonPath}'"`) 405 | this.showDebugError(e) 406 | throw e 407 | } 408 | 409 | verbose('package.json information has been loaded!') 410 | return this.packageInfo 411 | } 412 | 413 | /** 414 | * Generates module name based on file path 415 | * @param path path to be converted to module name 416 | * @param options additional conversion options 417 | */ 418 | private convertPathToModule( 419 | path: string, 420 | options: IConvertPathToModuleOptions = {}, 421 | ) { 422 | const { 423 | rootType = IBasePathType.tmp, 424 | noPrefix = false, 425 | noExtensionRemoval = false, 426 | noExistenceCheck = false, 427 | } = options 428 | 429 | const packageDetails = this.getPackageDetails() 430 | 431 | const fileExisted = 432 | noExistenceCheck || 433 | (!noExtensionRemoval && 434 | fs.existsSync(path) && 435 | fs.lstatSync(path).isFile()) 436 | 437 | if (rootType === IBasePathType.cwd) { 438 | path = relative(process.cwd(), path) 439 | } else if (rootType === IBasePathType.root) { 440 | path = relative(this.getRoot(), path) 441 | } else if (rootType === IBasePathType.tmp) { 442 | path = relative(this.getTempDir(), path) 443 | } 444 | 445 | if (!noPrefix) { 446 | path = `${packageDetails.name}/${path}` 447 | } 448 | 449 | path = path.replace(/\\/g, '/') 450 | 451 | if (fileExisted && !noExtensionRemoval) { 452 | path = path.replace(/\.[^.]+$/g, '') 453 | path = path.replace(/\.d$/g, '') 454 | } 455 | 456 | return path 457 | } 458 | 459 | /** 460 | * Loads generated per-file declaration files 461 | */ 462 | private loadTypings() { 463 | const result: IDeclarationMap = {} 464 | 465 | const declarationFiles = this.getDeclarationFiles() 466 | 467 | verbose('Loading declaration files and mapping to modules...') 468 | declarationFiles.forEach(file => { 469 | const moduleName = this.convertPathToModule(file) 470 | 471 | try { 472 | result[moduleName] = readFileSync(file, {encoding: 'utf8'}) 473 | } catch (e) { 474 | error(`Could not load declaration file '${file}'!`) 475 | this.showDebugError(e) 476 | throw e 477 | } 478 | }) 479 | 480 | verbose('Loaded declaration files and mapped to modules!') 481 | return result 482 | } 483 | 484 | private resolveImportSourcesAtLine( 485 | regexp: RegExp, 486 | line: string, 487 | moduleName: string, 488 | ) { 489 | const matches = line.match(regexp) 490 | 491 | if (matches && matches[2].startsWith('.')) { 492 | const relativePath = `../${matches[2]}` 493 | 494 | let resolvedModule = resolve(moduleName, relativePath) 495 | 496 | resolvedModule = this.convertPathToModule(resolvedModule, { 497 | rootType: IBasePathType.cwd, 498 | noPrefix: true, 499 | noExtensionRemoval: true, 500 | }) 501 | 502 | if (!this.moduleExists(resolvedModule)) { 503 | resolvedModule += '/index' 504 | } 505 | 506 | line = line.replace(regexp, `$1${resolvedModule}$3`) 507 | } 508 | 509 | return line 510 | } 511 | 512 | /** 513 | * Alters import sources to avoid relative addresses and default index usage 514 | * @param source import source to be resolved 515 | * @param moduleName name of module containing import 516 | */ 517 | private resolveImportSources(source: string, moduleName: string) { 518 | source = source.replace(/\r\n/g, '\n') 519 | source = source.replace(/\n\r/g, '\n') 520 | source = source.replace(/\r/g, '\n') 521 | 522 | let lines = source.split('\n') 523 | 524 | lines = lines.map(line => { 525 | line = this.resolveImportSourcesAtLine( 526 | /(from ['"])([^'"]+)(['"])/, 527 | line, 528 | moduleName, 529 | ) 530 | 531 | line = this.resolveImportSourcesAtLine( 532 | /(import\(['"])([^'"]+)(['"]\))/, 533 | line, 534 | moduleName, 535 | ) 536 | 537 | return line 538 | }) 539 | 540 | source = lines.join('\n') 541 | 542 | return source 543 | } 544 | 545 | /** 546 | * Combines typings into a single declaration source 547 | */ 548 | private async combineTypings() { 549 | const typings = this.loadTypings() 550 | await this.clearTempDir() 551 | 552 | this.moduleNames = Object.keys(typings) 553 | 554 | verbose('Combining typings into single file...') 555 | 556 | const sourceParts: string[] = [] 557 | 558 | Object.entries(typings).forEach(([moduleName, fileSource]) => { 559 | fileSource = fileSource.replace(/declare /g, '') 560 | fileSource = this.resolveImportSources(fileSource, moduleName) 561 | sourceParts.push( 562 | `declare module '${moduleName}' {\n${(fileSource as string).replace( 563 | /^./gm, 564 | ' $&', 565 | )}\n}`, 566 | ) 567 | }) 568 | 569 | verbose('Combined typings into a single file!') 570 | return sourceParts.join('\n') 571 | } 572 | 573 | /** 574 | * Verifies if module specified exists among known modules 575 | * @param moduleName name of module to be checked 576 | */ 577 | private moduleExists(moduleName: string) { 578 | return this.moduleNames.includes(moduleName) 579 | } 580 | 581 | /** 582 | * Adds alias for main NPM package file to generated .d.ts source 583 | * @param source generated .d.ts declaration source so far 584 | */ 585 | private addAlias(source: string) { 586 | verbose('Adding alias for main file of the package...') 587 | 588 | const packageDetails = this.getPackageDetails() 589 | const entry = this.getEntry() 590 | 591 | if (!entry) { 592 | error('No entry file is available!') 593 | throw new Error('No entry file is available!') 594 | } 595 | 596 | const mainFile = this.convertPathToModule(resolve(this.getRoot(), entry), { 597 | rootType: IBasePathType.root, 598 | noExistenceCheck: true, 599 | }) 600 | 601 | source += 602 | `\ndeclare module '${packageDetails.name}' {\n` + 603 | ` import main = require('${mainFile}');\n` + 604 | ' export = main;\n' + 605 | '}' 606 | 607 | verbose('Successfully created alias for main file!') 608 | 609 | return source 610 | } 611 | 612 | /** 613 | * Stores generated .d.ts declaration source into file 614 | * @param source generated .d.ts source 615 | */ 616 | private async storeResult(source: string) { 617 | const output = this.getOutput() 618 | const root = this.getRoot() 619 | const file = resolve(root, output) 620 | const folderPath = dirname(file) 621 | 622 | verbose('Ensuring that output folder exists...') 623 | debug(`Creating output folder: "${folderPath}"...`) 624 | 625 | try { 626 | await mkdir(folderPath) 627 | } catch (mkdirError) { 628 | error(`Failed to create "${folderPath}"!`) 629 | this.showDebugError(mkdirError) 630 | throw mkdirError 631 | } 632 | 633 | verbose('Output folder is ready!') 634 | verbose(`Storing typings into ${output} file...`) 635 | 636 | try { 637 | writeFileSync(file, source, {encoding: 'utf8'}) 638 | } catch (e) { 639 | error(`Failed to create ${output}!`) 640 | this.showDebugError(e) 641 | throw e 642 | } 643 | 644 | verbose(`Successfully created ${output} file!`) 645 | } 646 | } 647 | 648 | /** 649 | * Map of modules and their declarations 650 | */ 651 | export interface IDeclarationMap { 652 | [moduleNames: string]: string 653 | } 654 | 655 | /** 656 | * Types of base path used during path resolving 657 | */ 658 | export enum IBasePathType { 659 | /** 660 | * Base path is root of targeted project 661 | */ 662 | root = 'root', 663 | 664 | /** 665 | * Base path is tmp directory 666 | */ 667 | tmp = 'tmp', 668 | 669 | /** 670 | * Base path is CWD 671 | */ 672 | cwd = 'cwd', 673 | } 674 | 675 | /** 676 | * Additional conversion options 677 | */ 678 | export interface IConvertPathToModuleOptions { 679 | /** 680 | * Type of base path used during path resolving 681 | */ 682 | rootType?: IBasePathType 683 | 684 | /** 685 | * Disables addition of module name as prefix for module name 686 | */ 687 | noPrefix?: boolean 688 | 689 | /** 690 | * Disables extension removal 691 | */ 692 | noExtensionRemoval?: boolean 693 | 694 | /** 695 | * Disables existence check and assumes that file exists 696 | */ 697 | noExistenceCheck?: boolean 698 | } 699 | --------------------------------------------------------------------------------