├── .eslintrc ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .node-dev.json ├── .travis.yml ├── .vscode └── tasks.json ├── .yalcignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docker-compose.yml ├── icons ├── node_error.png └── node_info.png ├── package.json ├── src ├── bin.ts ├── cfg.ts ├── check-file-exists.ts ├── child-require-hook.ts ├── compiler.ts ├── dedupe.ts ├── get-compiled-path.ts ├── get-cwd.ts ├── hook.ts ├── index.ts ├── ipc.ts ├── log.ts ├── notify.ts ├── resolveMain.ts └── wrap.ts ├── test ├── fixture │ ├── .rcfile │ ├── add-req.ts │ ├── dep-ts-error.ts │ ├── dep.ts │ ├── dir-test │ │ ├── imported.js │ │ ├── index.ts │ │ └── tsconfig.json │ ├── file.json │ ├── folder │ │ └── some-file │ ├── import-json.ts │ ├── js-module.js │ ├── nameof.ts │ ├── node_modules │ │ └── package │ │ │ ├── index.js │ │ │ └── node_modules │ │ │ └── level2-package │ │ │ └── index.js │ ├── not-found │ │ ├── js-with-not-found.js │ │ └── with-not-found-js.ts │ ├── prefer │ │ ├── prefer-dep.js │ │ ├── prefer-dep.ts │ │ ├── prefer.js │ │ └── prefer.ts │ ├── req-package.ts │ ├── simple.ts │ ├── to-transform.ts │ ├── uncaught-handler.ts │ ├── with-error.ts │ └── with-not-found.ts ├── manual │ ├── add-require-2.ts │ ├── add-require.js │ ├── big.ts │ ├── dep-interface.ts │ ├── dep.ts │ ├── run.ts │ └── test-script.ts ├── spawn.ts ├── transformer.ts └── tsnd.test.ts ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@whitecolor/eslint-config"] 3 | } 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Issue description 2 | 3 | ## Context 4 | 5 | *OS version (is it docker or host?), ts-node-dev version* 6 | 7 | *Did you try to run with [ts-node](https://github.com/TypeStrong/ts-node)*? 8 | 9 | *Did you try to run with `--files` option enabled?* 10 | 11 | *Did you try to run with `--debug` option enabled?* 12 | 13 | *Do you have a repro example (git repo) with simple steps to reproduce your problem?* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules 3 | /lib 4 | *.log 5 | .ts-node 6 | .tmp -------------------------------------------------------------------------------- /.node-dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "clear": true 3 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # sudo: false 2 | language: node_js 3 | node_js: 4 | - 12.0.0 5 | - 14.0.0 6 | script: yarn ci 7 | branches: 8 | only: 9 | - master 10 | cache: 11 | yarn: true 12 | -------------------------------------------------------------------------------- /.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": "tsc", 8 | "command": "node", 9 | // "isShellCommand": true, 10 | "args": [ 11 | "./node_modules/typescript/lib/tsc.js", 12 | "-w", 13 | "-p", 14 | "./tsconfig.build.json" 15 | ], 16 | "presentation": { 17 | "echo": true, 18 | "reveal": "silent", 19 | "focus": false, 20 | "panel": "shared", 21 | "showReuseMessage": true, 22 | "clear": false, 23 | "revealProblems": "always" 24 | }, 25 | "runOptions": { 26 | "runOn": "folderOpen" 27 | }, 28 | //"showOutput": "silent", 29 | "isBackground": true, 30 | "problemMatcher": "$tsc-watch" 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /.yalcignore: -------------------------------------------------------------------------------- 1 | README.md -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # ts-node-dev changelog 2 | 3 | ## 1.1.3 (2021-02-25) 4 | 5 | - fix: update bin scripts paths 6 | 7 | ## 1.1.2 (2021-02-25) 8 | 9 | - fix: update chokidar version 10 | 11 | 12 | ## 1.1.1 (2020-12-10) 13 | 14 | - fix: remove duplicate compilation call 15 | 16 | ## 1.1.0 (2020-12-10) 17 | 18 | - fix: not kill child process if it has its own exception handlers 19 | - fix: use either `process.send` or `writeFile` fallback 20 | - fix: prevent handling of duplicate compilation requests 21 | 22 | ## 1.0.0 (2020-10-17) 23 | 24 | - upgrade to ts-node v9 25 | 26 | ## 1.0.0-pre.65 (2020-10-15) 27 | 28 | - add --quiet option to silent [INFO] messages 29 | 30 | ## 1.0.0-pre.63 (2020-09-22) 31 | 32 | - fix --cache-directory flag 33 | 34 | ## 1.0.0-pre.62 (2020-08-22) 35 | 36 | - fix child fork override 37 | 38 | ## 1.0.0-pre.61 (2020-08-26) 39 | 40 | - fix terminal clear 41 | 42 | ## 1.0.0-pre.60 (2020-08-22) 43 | 44 | - full migration to typescript src 45 | - fixes of require.extensions behavior in compiler 46 | - child error stack output is back 47 | 48 | ## 1.0.0-pre.59 (2020-08-20) 49 | 50 | - fix handing require extensions (#185, #196) 51 | 52 | ## 1.0.0-pre.58 (2020-08-18) 53 | 54 | - show versions only on first start 55 | 56 | ## 1.0.0-pre.57 (2020-08-13) 57 | 58 | - fix `--deps` flag 59 | - add `--deps-level` flag 60 | - remove setting default NODE_ENV 61 | - add process.env.TS_NODE_DEV = 'true' 62 | 63 | ## 1.0.0-pre.56 (2020-07-24) 64 | 65 | - add `node-notifier` from `peerDependencies` make optional 66 | 67 | ## 1.0.0-pre.55 (2020-07-24) 68 | 69 | - remove `node-notifier` from `peerDependencies` 70 | 71 | ## 1.0.0-pre.54 (2020-07-23) 72 | 73 | - handle JSX extension, when `allowJs` enabled 74 | 75 | ## 1.0.0-pre.53 (2020-07-23) 76 | 77 | - move `node-notifier` to `peerDependencies` 78 | - add --script-mode flag handling 79 | 80 | ## 1.0.0-pre.52 81 | 82 | 83 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2014–2015 Felix Gnass 4 | Copyright (c) 2015 Daniel Gasienica 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ts-node-dev 2 | 3 | > Tweaked version of [node-dev](https://github.com/fgnass/node-dev) that uses [ts-node](https://github.com/TypeStrong/ts-node) under the hood. 4 | 5 | It restarts target node process when any of required files changes (as standard `node-dev`) but shares [Typescript](https://github.com/Microsoft/TypeScript/) compilation process between restarts. This significantly increases speed of restarting comparing to `node-dev -r ts-node/register ...`, `nodemon -x ts-node ...` variations because there is no need to instantiate `ts-node` compilation each time. 6 | 7 | ## Install 8 | 9 | ![npm (scoped)](https://img.shields.io/npm/v/ts-node-dev.svg?maxAge=86400) [![Build Status](https://travis-ci.com/wclr/ts-node-dev.svg?branch=master)](https://travis-ci.com/wclr/ts-node-dev) 10 | 11 | ``` 12 | yarn add ts-node-dev --dev 13 | ``` 14 | 15 | ``` 16 | npm i ts-node-dev --save-dev 17 | ``` 18 | 19 | ## Usage 20 | 21 | ``` 22 | ts-node-dev [node-dev|ts-node flags] [ts-node-dev flags] [node cli flags] [--] [script] [script arguments] 23 | ``` 24 | 25 | So you just combine [node-dev](https://github.com/fgnass/node-dev) and [ts-node](https://github.com/TypeStrong/ts-node) options (see docs of those packages): 26 | 27 | ``` 28 | ts-node-dev --respawn --transpile-only server.ts 29 | ``` 30 | 31 | There is also short alias `tsnd` for running `ts-node-dev`: 32 | 33 | ``` 34 | tsnd --respawn server.ts 35 | ``` 36 | 37 | Look up flags and options can be used [in ts-node's docs](https://github.com/TypeStrong/ts-node#cli-and-programmatic-options). 38 | 39 | **Also there are additional options specific to `ts-node-dev`:** 40 | 41 | * `--ignore-watch` - (default: []) - files/folders to be [ignored by `node-dev`](https://github.com/fgnass/node-dev#ignore-paths). **But this behaviour is enhanced:** it also supports regular expression in the ignore strings and will check absolute paths of required files for match. 42 | 43 | * `--deps` - Also watch `node_modules`; by default watching is turned off 44 | 45 | * `--debug` - Some additional [DEBUG] output 46 | * `--quiet` - Silent [INFO] messages 47 | * `--interval` - Polling interval (ms) - DOESN'T WORK CURRENTLY 48 | * `--debounce` - Debounce file change events (ms, non-polling mode) 49 | * `--clear` (`--cls`) - Will clear screen on restart 50 | * `--watch` - Explicitly add arbitrary files or folders to watch and restart on change (list separated by commas, [chokidar](https://github.com/paulmillr/chokidar) patterns) 51 | * `--exit-child` - Adds 'SIGTERM' exit handler in a child process. 52 | * `--rs` - Allow to restart with "rs" line entered in stdio, disabled by default. 53 | * `--notify` - to display desktop-notifications (Notifications are only displayed if `node-notifier` is installed). 54 | * `--cache-directory` - tmp dir which is used to keep the compiled sources (by default os tmp directory is used) 55 | 56 | If you need to detect that you are running with `ts-node-dev`, check if `process.env.TS_NODE_DEV` is set. 57 | 58 | 59 | **Points of notice:** 60 | 61 | - If you want desktop-notifications you should install `node-notifier` package and use `--notify` flag. 62 | 63 | - Especially for large code bases always consider running with `--transpile-only` flag which is normal for dev workflow and will speed up things greatly. Note, that `ts-node-dev` will not put watch handlers on TS files that contain only types/interfaces (used only for type checking) - this is current limitation by design. 64 | 65 | - `--ignore-watch` will NOT affect files ignored by TS compilation. Use `--ignore` option (or `TS_NODE_IGNORE` env variable) to pass **RegExp strings** for filtering files that should not be compiled, by default `/node_modules/` are ignored. 66 | 67 | - Unknown flags (`node` cli flags are considered to be so) are treated like string value flags by default. The right solution to avoid ambiguity is to separate script name from option flags with `--`, for example: 68 | 69 | ``` 70 | ts-node-dev --inspect -- my-script.ts 71 | ``` 72 | 73 | - The good thing is that `ts-node-dev` watches used `tsconfig.json` file, and will reinitialize compilation on its change, but you have to restart the process manually when you update used version of `typescript` or make any other changes that may effect compilation results. 74 | 75 | ## Issues 76 | 77 | If you have an issue, please create one. But, before: 78 | - try to check if there exits alike issues. 79 | - try to run your code with just [ts-node](https://github.com/TypeStrong/ts-node) 80 | - try to run your code with `--files` option enabled ([see ts-node docs](https://github.com/TypeStrong/ts-node#help-my-types-are-missing)) 81 | - try to run it with `--debug` flag and see the output 82 | - try to make create repro example 83 | 84 | ## Versioning 85 | 86 | Currently versioning is not stable and it is still treated as pre-release. You might expect some options API changes. If you want to avoid unexpected problems it is recommended to fixate the installed version and update only in case of issues, you may consult [CHANGELOG](CHANGELOG.md) for updates. 87 | 88 | ## License 89 | 90 | MIT. 91 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.0' 2 | volumes: 3 | node10modules: null 4 | node10tmp: null 5 | node14modules: null 6 | node14tmp: null 7 | services: 8 | node10: 9 | image: node:10.21.0-jessie-slim 10 | working_dir: /package 11 | command: sh -c "yarn --frozen-lockfile && yarn test" 12 | volumes: 13 | - ./:/package 14 | - node10modules:/package/node_modules 15 | - node10tmp:/package/.tmp 16 | 17 | node14: 18 | image: node:14-stretch-slim 19 | working_dir: /package 20 | command: sh -c "yarn --frozen-lockfile && yarn test" 21 | volumes: 22 | - ./:/package 23 | - node14modules:/package/node_modules 24 | - node14tmp:/package/.tmp 25 | -------------------------------------------------------------------------------- /icons/node_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wclr/ts-node-dev/32bdc92458a59f66bcbd36e87de2a793f529a825/icons/node_error.png -------------------------------------------------------------------------------- /icons/node_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wclr/ts-node-dev/32bdc92458a59f66bcbd36e87de2a793f529a825/icons/node_info.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-node-dev", 3 | "version": "2.0.0", 4 | "description": "Compiles your TS app and restarts when files are modified.", 5 | "keywords": [ 6 | "restart", 7 | "reload", 8 | "supervisor", 9 | "monitor", 10 | "watch" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "http://github.com/whitecolor/ts-node-dev.git" 15 | }, 16 | "license": "MIT", 17 | "bin": { 18 | "ts-node-dev": "lib/bin.js", 19 | "tsnd": "lib/bin.js" 20 | }, 21 | "main": "lib", 22 | "files": [ 23 | "icons", 24 | "lib" 25 | ], 26 | "prettier": { 27 | "singleQuote": true, 28 | "semi": false 29 | }, 30 | "engines": { 31 | "node": ">=0.8.0" 32 | }, 33 | "scripts": { 34 | "ts-node-dev": "node ./lib/bin", 35 | "build": "tsc -p tsconfig.build.json", 36 | "release": "np", 37 | "test": "yarn build && ts-node -T node_modules/mocha/bin/mocha test/*.test.ts", 38 | "test-dev": "yarn ts-node-dev -T --respawn --deps --watch lib node_modules/mocha/bin/mocha test/*.test.ts --output", 39 | "test-docker": "docker-compose up", 40 | "ci": "yarn test", 41 | "ci-local": "docker run --name travis-debug -dit quay.io/travisci/ci-nodejs", 42 | "manual": "yarn ts-node test/manual/run.ts" 43 | }, 44 | "dependencies": { 45 | "chokidar": "^3.5.1", 46 | "dynamic-dedupe": "^0.3.0", 47 | "minimist": "^1.2.6", 48 | "mkdirp": "^1.0.4", 49 | "resolve": "^1.0.0", 50 | "rimraf": "^2.6.1", 51 | "source-map-support": "^0.5.12", 52 | "tree-kill": "^1.2.2", 53 | "ts-node": "^10.4.0", 54 | "tsconfig": "^7.0.0" 55 | }, 56 | "devDependencies": { 57 | "@types/chai": "^4.2.12", 58 | "@types/chokidar": "^2.1.3", 59 | "@types/fs-extra": "^9.0.1", 60 | "@types/minimist": "^1.2.0", 61 | "@types/mkdirp": "^1.0.1", 62 | "@types/mocha": "github:whitecolor/mocha-types", 63 | "@types/node": "^14.6.0", 64 | "@types/rimraf": "^3.0.0", 65 | "@types/tape": "^4.13.0", 66 | "@types/touch": "^3.1.1", 67 | "@types/ts-nameof": "^4.2.1", 68 | "@whitecolor/eslint-config": "^1.0.0", 69 | "chai": "^4.2.0", 70 | "chalk": "^4.1.0", 71 | "coffee-script": "^1.8.0", 72 | "eslint": "^7.7.0", 73 | "esm": "^3.2.22", 74 | "fs-extra": "^9.0.1", 75 | "mocha": "^8.1.1", 76 | "np": "^7.6.1", 77 | "tap": "^5.2.0", 78 | "tape": "^5.0.1", 79 | "touch": "^1.0.0", 80 | "ts-nameof": "^5.0.0", 81 | "tsconfig-paths": "^3.3.1", 82 | "ttypescript": "^1.5.10", 83 | "typescript": "^3.9.5" 84 | }, 85 | "peerDependencies": { 86 | "node-notifier": "*", 87 | "typescript": "*" 88 | }, 89 | "peerDependenciesMeta": { 90 | "node-notifier": { 91 | "optional": true 92 | } 93 | }, 94 | "np": { 95 | "yarn": false, 96 | "cleanup": false 97 | }, 98 | "publishConfig": { 99 | "registry": "https://registry.npmjs.org" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/bin.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { runDev } from '.' 4 | import minimist from 'minimist' 5 | 6 | const nodeArgs: string[] = [] 7 | const unknown: string[] = [] 8 | 9 | const devArgs = process.argv.slice(2, 100) 10 | 11 | const tsNodeFlags = { 12 | boolean: [ 13 | 'scope', 14 | 'emit', 15 | 'files', 16 | 'pretty', 17 | 'transpile-only', 18 | 'prefer-ts-exts', 19 | 'prefer-ts', 20 | 'log-error', 21 | 'skip-project', 22 | 'skip-ignore', 23 | 'compiler-host', 24 | 'script-mode', 25 | ], 26 | 27 | string: [ 28 | 'compiler', 29 | 'project', 30 | 'ignore', 31 | 'ignore-diagnostics', 32 | 'compiler-options', 33 | 'scopeDir', 34 | 'transpiler' 35 | ], 36 | } 37 | 38 | const tsNodeAlias = { 39 | 'transpile-only': 'T', 40 | 'compiler-host': 'H', 41 | ignore: 'I', 42 | 'ignore-diagnostics': 'D', 43 | 'compiler-options': 'O', 44 | compiler: 'C', 45 | project: 'P', 46 | 'script-mode': 's', 47 | } 48 | 49 | type TSNodeOptions = { 50 | project: string 51 | compilerOptions: any 52 | 'compiler-options': any 53 | 'prefer-ts-exts': boolean 54 | ignore?: string 55 | 56 | dir: string 57 | 'script-mode': boolean 58 | emit: boolean 59 | files: boolean 60 | 'transpile-only': boolean 61 | pretty: boolean 62 | scope: boolean 63 | scopeDir: string, 64 | transpiler: string 65 | 'log-error': boolean 66 | 'skip-project': boolean 67 | 'skip-ignore': boolean 68 | compiler: string 69 | 'compiler-host': boolean 70 | 'ignore-diagnostics': string 71 | } 72 | 73 | const devFlags = { 74 | boolean: [ 75 | 'deps', 76 | 'all-deps', 77 | 'dedupe', 78 | 'fork', 79 | 'exec-check', 80 | 'debug', 81 | 'poll', 82 | 'respawn', 83 | 'notify', 84 | 'tree-kill', 85 | 'clear', 86 | 'cls', 87 | 'exit-child', 88 | 'error-recompile', 89 | 'quiet', 90 | 'rs', 91 | ], 92 | string: [ 93 | 'dir', 94 | 'deps-level', 95 | 'compile-timeout', 96 | 'ignore-watch', 97 | 'interval', 98 | 'debounce', 99 | 'watch', 100 | 'cache-directory', 101 | ], 102 | } 103 | 104 | type DevOptions = { 105 | poll: boolean 106 | debug: boolean 107 | fork: boolean 108 | watch: string 109 | interval: string 110 | rs: boolean 111 | deps: boolean 112 | dedupe: boolean 113 | respawn: boolean 114 | notify: boolean 115 | clear: boolean 116 | cls: boolean 117 | 'ignore-watch': string 118 | 'all-deps': boolean 119 | 'deps-level': string 120 | 'compile-timeout': string 121 | 'exec-check': boolean 122 | 'exit-child': boolean 123 | 'cache-directory': string 124 | 'error-recompile': boolean 125 | quiet: boolean 126 | 'tree-kill': boolean 127 | } 128 | 129 | export type Options = { 130 | priorNodeArgs: string[] 131 | _: string[] 132 | } & DevOptions & 133 | TSNodeOptions 134 | 135 | const opts = minimist(devArgs, { 136 | stopEarly: true, 137 | boolean: [...devFlags.boolean, ...tsNodeFlags.boolean], 138 | string: [...devFlags.string, ...tsNodeFlags.string], 139 | alias: { 140 | ...tsNodeAlias, 141 | 'prefer-ts-exts': 'prefer-ts', 142 | }, 143 | default: { 144 | fork: true, 145 | }, 146 | unknown: function (arg) { 147 | unknown.push(arg) 148 | return true 149 | }, 150 | }) as Options 151 | 152 | const script = opts._[0] 153 | const scriptArgs = opts._.slice(1) 154 | 155 | opts.priorNodeArgs = [] 156 | 157 | unknown.forEach(function (arg) { 158 | if (arg === script || nodeArgs.indexOf(arg) >= 0) return 159 | 160 | const argName = arg.replace(/^-+/, '') 161 | // fix this 162 | const argOpts = (opts as any)[argName] 163 | const argValues = Array.isArray(argOpts) ? argOpts : [argOpts] 164 | argValues.forEach(function (argValue) { 165 | if ((arg === '-r' || arg === '--require') && argValue === 'esm') { 166 | opts.priorNodeArgs.push(arg, argValue) 167 | return false 168 | } 169 | nodeArgs.push(arg) 170 | if (typeof argValue === 'string') { 171 | nodeArgs.push(argValue) 172 | } 173 | }) 174 | }) 175 | 176 | if (!script) { 177 | // eslint-disable-next-line no-console 178 | console.log('ts-node-dev: no script to run provided') 179 | // eslint-disable-next-line no-console 180 | console.log('Usage: ts-node-dev [options] script [arguments]\n') 181 | process.exit(1) 182 | } 183 | 184 | runDev(script, scriptArgs, nodeArgs, opts) 185 | -------------------------------------------------------------------------------- /src/cfg.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import { Options } from './bin' 4 | import { type } from 'os' 5 | 6 | function read(dir: string) { 7 | const f = path.resolve(dir, '.node-dev.json') 8 | return fs.existsSync(f) ? JSON.parse(fs.readFileSync(f, 'utf-8')) : null 9 | } 10 | 11 | function resolvePath(unresolvedPath: string) { 12 | return path.resolve(process.cwd(), unresolvedPath) 13 | } 14 | 15 | export type Config = { 16 | vm: boolean 17 | fork: boolean 18 | notify: boolean 19 | deps: number 20 | timestamp: number 21 | clear: boolean 22 | dedupe: boolean 23 | ignore: string[] 24 | respawn: boolean 25 | debug: boolean 26 | quiet: boolean 27 | extensions: Record 28 | } 29 | 30 | export const makeCfg = (main: string, opts: Partial): Config => { 31 | const dir = main ? path.dirname(main) : '.' 32 | const userDir = process.env.HOME || process.env.USERPROFILE 33 | const c = read(dir) || read(process.cwd()) || (userDir && read(userDir)) || {} 34 | 35 | c.deps = parseInt(opts['deps-level'] || '') || 0 36 | if (typeof c.depsLevel === 'number') c.deps = c.depsLevel 37 | 38 | if (opts) { 39 | // Overwrite with CLI opts ... 40 | if (opts['deps'] || opts['all-deps']) c.deps = -1 41 | if (opts.dedupe) c.dedupe = true 42 | if (opts.respawn) c.respawn = true 43 | if (opts.notify === false) c.notify = false 44 | if (opts.clear || opts.cls) c.clear = true 45 | c.fork = opts.fork 46 | } 47 | 48 | const ignoreWatchItems: string[] = opts['ignore-watch'] 49 | ? ([] as string[]) 50 | .concat(opts['ignore-watch'] as string) 51 | .map((_) => _.trim()) 52 | : [] 53 | const ignoreWatch: string[] = ignoreWatchItems.concat(c.ignore || []) 54 | opts.debug && console.log('Ignore watch:', ignoreWatch) 55 | const ignore = ignoreWatch.concat(ignoreWatch.map(resolvePath)) 56 | return { 57 | vm: c.vm !== false, 58 | fork: c.fork !== false, 59 | notify: c.notify !== false, 60 | deps: c.deps, 61 | timestamp: c.timestamp || (c.timestamp !== false && 'HH:MM:ss'), 62 | clear: !!c.clear, 63 | dedupe: !!c.dedupe, 64 | ignore: ignore, 65 | respawn: c.respawn || false, 66 | debug: !!opts.debug, 67 | quiet: !!opts.quiet, 68 | extensions: c.extensions, 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/check-file-exists.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | const filePath = process.argv[2] 3 | 4 | const handler = function (stat: fs.Stats) { 5 | if (stat && stat.birthtime.getTime() > 0) { 6 | process.exit(0) 7 | } 8 | } 9 | 10 | fs.watchFile(filePath, { interval: 100 }, handler) 11 | fs.stat(filePath, function (err, stat) { 12 | handler(stat) 13 | }) 14 | -------------------------------------------------------------------------------- /src/child-require-hook.ts: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const getCompiledPath = require('./get-compiled-path').getCompiledPath 3 | const sep = require('path').sep 4 | const join = require('path').join 5 | const extname = require('path').extname 6 | const execSync = require('child_process').execSync 7 | const Module = require('module') 8 | const compilationId = '' 9 | const timeThreshold = 0 10 | const allowJs = false 11 | const compiledDir = '' 12 | const preferTs = false 13 | const ignore = [/node_modules/] 14 | const readyFile = '' 15 | const execCheck = false 16 | const exitChild = false 17 | const sourceMapSupportPath = '' 18 | const libPath = '' 19 | 20 | const checkFileScript = join(__dirname, 'check-file-exists.js') 21 | 22 | const waitForFile = function (fileName: string) { 23 | const start = new Date().getTime() 24 | while (true) { 25 | const exists = execCheck 26 | ? execSync(['node', checkFileScript, '"' + fileName + '"'].join(' '), { 27 | stdio: 'inherit', 28 | }) || true 29 | : fs.existsSync(fileName) 30 | 31 | if (exists) { 32 | return 33 | } 34 | const passed = new Date().getTime() - start 35 | if (timeThreshold && passed > timeThreshold) { 36 | throw new Error('Could not require ' + fileName) 37 | } 38 | } 39 | } 40 | 41 | const sendFsCompileRequest = (fileName: string, compiledPath: string) => { 42 | const compileRequestFile = [compiledDir, compilationId + '.req'].join(sep) 43 | fs.writeFileSync(compileRequestFile, [fileName, compiledPath].join('\n')) 44 | } 45 | 46 | const compile = (code: string, fileName: string) => { 47 | const compiledPath = getCompiledPath(code, fileName, compiledDir) 48 | if (process.send) { 49 | try { 50 | process.send({ 51 | compile: fileName, 52 | compiledPath: compiledPath, 53 | }) 54 | } catch (e) { 55 | console.warn('Error while sending compile request via process.send') 56 | sendFsCompileRequest(fileName, compiledPath) 57 | } 58 | } else { 59 | sendFsCompileRequest(fileName, compiledPath) 60 | } 61 | 62 | waitForFile(compiledPath + '.done') 63 | const compiled = fs.readFileSync(compiledPath, 'utf-8') 64 | return compiled 65 | } 66 | 67 | function registerExtensions(extensions: string[]) { 68 | extensions.forEach(function (ext) { 69 | const old = require.extensions[ext] || require.extensions['.js'] 70 | require.extensions[ext] = function (m: any, fileName) { 71 | const _compile = m._compile 72 | m._compile = function (code: string, fileName: string) { 73 | return _compile.call(this, compile(code, fileName), fileName) 74 | } 75 | return old(m, fileName) 76 | } 77 | }) 78 | if (preferTs) { 79 | const reorderRequireExtension = (ext: string) => { 80 | const old = require.extensions[ext] 81 | delete require.extensions[ext] 82 | require.extensions[ext] = old 83 | } 84 | const order = ['.ts', '.tsx'].concat( 85 | Object.keys(require.extensions).filter((_) => _ !== '.ts' && _ !== '.tsx') 86 | ) 87 | order.forEach(function (ext) { 88 | reorderRequireExtension(ext) 89 | }) 90 | } 91 | } 92 | 93 | function isFileInNodeModules(fileName: string) { 94 | return fileName.indexOf(sep + 'node_modules' + sep) >= 0 95 | } 96 | 97 | function registerJsExtension() { 98 | const old = require.extensions['.js'] 99 | // handling preferTs probably redundant after reordering 100 | if (allowJs) { 101 | require.extensions['.jsx'] = require.extensions['.js'] = function ( 102 | m: any, 103 | fileName 104 | ) { 105 | if (fileName.indexOf(libPath) === 0) { 106 | return old(m, fileName) 107 | } 108 | const tsCode: string | undefined = undefined 109 | const tsFileName = '' 110 | const _compile = m._compile 111 | const isIgnored = 112 | ignore && 113 | ignore.reduce(function (res, ignore) { 114 | return res || ignore.test(fileName) 115 | }, false) 116 | const ext = extname(fileName) 117 | if (tsCode !== undefined || (allowJs && !isIgnored && ext == '.js')) { 118 | m._compile = function (code: string, fileName: string) { 119 | if (tsCode !== undefined) { 120 | code = tsCode 121 | fileName = tsFileName 122 | } 123 | return _compile.call(this, compile(code, fileName), fileName) 124 | } 125 | } 126 | 127 | return old(m, fileName) 128 | } 129 | } 130 | } 131 | 132 | const sourceMapRequire = Module.createRequire 133 | ? Module.createRequire(sourceMapSupportPath) 134 | : require 135 | 136 | sourceMapRequire(sourceMapSupportPath).install({ 137 | hookRequire: true, 138 | }) 139 | 140 | registerJsExtension() 141 | registerExtensions(['.ts', '.tsx']) 142 | 143 | if (readyFile) { 144 | const time = new Date().getTime() 145 | while (!fs.existsSync(readyFile)) { 146 | if (new Date().getTime() - time > 5000) { 147 | throw new Error('Waiting ts-node-dev ready file failed') 148 | } 149 | } 150 | } 151 | 152 | if (exitChild) { 153 | process.on('SIGTERM', function () { 154 | console.log('Child got SIGTERM, exiting.') 155 | process.exit() 156 | }) 157 | } 158 | 159 | module.exports.registerExtensions = registerExtensions 160 | -------------------------------------------------------------------------------- /src/compiler.ts: -------------------------------------------------------------------------------- 1 | import * as tsNode from 'ts-node' 2 | 3 | import fs from 'fs' 4 | import path from 'path' 5 | import os from 'os' 6 | import mkdirp from 'mkdirp' 7 | import rimraf from 'rimraf' 8 | import { resolveSync } from 'tsconfig' 9 | import { Options } from './bin' 10 | import { getCompiledPath } from './get-compiled-path' 11 | import { Log } from './log' 12 | import { getCwd } from './get-cwd' 13 | 14 | const fixPath = (p: string) => p.replace(/\\/g, '/').replace(/\$/g, '$$$$') 15 | 16 | const sourceMapSupportPath = require.resolve('source-map-support') 17 | 18 | const compileExtensions = ['.ts', '.tsx'] 19 | const cwd = process.cwd() 20 | const compilationInstanceStamp = Math.random().toString().slice(2) 21 | 22 | const originalJsHandler = require.extensions['.js'] 23 | 24 | export type CompileParams = { 25 | code?: string 26 | compile: string 27 | compiledPath: string 28 | } 29 | 30 | const parse = (value: string | undefined): object | undefined => { 31 | return typeof value === 'string' ? JSON.parse(value) : undefined 32 | } 33 | 34 | function split(value: string | undefined) { 35 | return typeof value === 'string' 36 | ? value.split(/ *, */g).filter((v) => v !== '') 37 | : undefined 38 | } 39 | 40 | export const makeCompiler = ( 41 | options: Options, 42 | { 43 | log, 44 | restart, 45 | }: { 46 | log: Log 47 | restart: (fileName: string) => void 48 | } 49 | ) => { 50 | let _errorCompileTimeout: ReturnType 51 | let allowJs = false 52 | 53 | const project = options['project'] 54 | const tsConfigPath = 55 | resolveSync(cwd, typeof project === 'string' ? project : undefined) || '' 56 | 57 | const compiledPathsHash: Record = {} 58 | 59 | const tmpDir = options['cache-directory'] 60 | ? path.resolve(options['cache-directory']) 61 | : fs.mkdtempSync(path.join(os.tmpdir(), '.ts-node')) 62 | 63 | const writeChildHookFile = (options: Options) => { 64 | const compileTimeout = parseInt(options['compile-timeout']) 65 | 66 | const getIgnoreVal = (ignore: string) => { 67 | const ignoreVal = 68 | !ignore || ignore === 'false' 69 | ? 'false' 70 | : '[' + 71 | ignore 72 | .split(/,/) 73 | .map((i) => i.trim()) 74 | .map((ignore) => 'new RegExp("' + ignore + '")') 75 | .join(', ') + 76 | ']' 77 | return ignoreVal 78 | } 79 | 80 | const varDecl = (name: string, value: string) => `var ${name} = '${value}'` 81 | 82 | const replacements: string[][] = [ 83 | compileTimeout ? ['10000', compileTimeout.toString()] : null, 84 | allowJs ? ['allowJs = false', 'allowJs = true'] : null, 85 | options['prefer-ts-exts'] 86 | ? ['preferTs = false', 'preferTs = true'] 87 | : null, 88 | options['exec-check'] ? ['execCheck = false', 'execCheck = true'] : null, 89 | options['exit-child'] ? ['exitChild = false', 'exitChild = true'] : null, 90 | options['ignore'] !== undefined 91 | ? [ 92 | 'var ignore = [/node_modules/]', 93 | 'var ignore = ' + getIgnoreVal(options['ignore']), 94 | ] 95 | : null, 96 | [ 97 | varDecl('compilationId', ''), 98 | varDecl('compilationId', getCompilationId()), 99 | ], 100 | [varDecl('compiledDir', ''), varDecl('compiledDir', getCompiledDir())], 101 | [ 102 | './get-compiled-path', 103 | fixPath(path.join(__dirname, 'get-compiled-path')), 104 | ], 105 | [ 106 | varDecl('readyFile', ''), 107 | varDecl('readyFile', getCompilerReadyFilePath()), 108 | ], 109 | [ 110 | varDecl('sourceMapSupportPath', ''), 111 | varDecl('sourceMapSupportPath', fixPath(sourceMapSupportPath)), 112 | ], 113 | [ 114 | varDecl('libPath', ''), 115 | varDecl('libPath', __dirname.replace(/\\/g, '\\\\')), 116 | ], 117 | ['__dirname', '"' + fixPath(__dirname) + '"'], 118 | ] 119 | .filter((_) => !!_) 120 | .map((_) => _!) 121 | 122 | const fileText = fs.readFileSync( 123 | path.join(__dirname, 'child-require-hook.js'), 124 | 'utf-8' 125 | ) 126 | 127 | const fileData = replacements.reduce((text, [what, to]) => { 128 | return text.replace(what, to) 129 | }, fileText) 130 | 131 | fs.writeFileSync(getChildHookPath(), fileData) 132 | } 133 | 134 | const init = () => { 135 | registerTsNode() 136 | 137 | /* clean up compiled on each new init*/ 138 | rimraf.sync(getCompiledDir()) 139 | createCompiledDir() 140 | 141 | // check if `allowJs` compiler option enable 142 | // (.js handler was changed while ts-node registration) 143 | allowJs = require.extensions['.js'] !== originalJsHandler 144 | if (allowJs) { 145 | compileExtensions.push('.js', '.jsx') 146 | } 147 | 148 | writeChildHookFile(options) 149 | } 150 | 151 | const getCompilationId = () => { 152 | return compilationInstanceStamp 153 | } 154 | const createCompiledDir = () => { 155 | const compiledDir = getCompiledDir() 156 | if (!fs.existsSync(compiledDir)) { 157 | mkdirp.sync(getCompiledDir()) 158 | } 159 | } 160 | const getCompiledDir = () => { 161 | return path.join(tmpDir, 'compiled').replace(/\\/g, '/') 162 | } 163 | const getCompileReqFilePath = () => { 164 | return path.join(getCompiledDir(), getCompilationId() + '.req') 165 | } 166 | const getCompilerReadyFilePath = () => { 167 | return path 168 | .join(os.tmpdir(), 'ts-node-dev-ready-' + compilationInstanceStamp) 169 | .replace(/\\/g, '/') 170 | } 171 | 172 | const getChildHookPath = () => { 173 | return path 174 | .join(os.tmpdir(), 'ts-node-dev-hook-' + compilationInstanceStamp + '.js') 175 | .replace(/\\/g, '/') 176 | } 177 | const writeReadyFile = () => { 178 | fs.writeFileSync(getCompilerReadyFilePath(), '') 179 | } 180 | 181 | const clearErrorCompile = () => { 182 | clearTimeout(_errorCompileTimeout) 183 | } 184 | const registerTsNode = () => { 185 | Object.keys(compiledPathsHash).forEach((key) => { 186 | delete compiledPathsHash[key] 187 | }) 188 | // revert back original handler extensions 189 | // in case of re-registering 190 | ;['.js', '.jsx', '.ts', '.tsx'].forEach(function (ext) { 191 | require.extensions[ext] = originalJsHandler 192 | }) 193 | 194 | const scriptPath = options._.length 195 | ? path.resolve(cwd, options._[0]) 196 | : undefined 197 | 198 | tsNode.register({ 199 | // --dir does not work (it gives a boolean only) so we only check for script-mode 200 | dir: getCwd(options['dir'], options['script-mode'], scriptPath), 201 | scope: options['scope'], 202 | scopeDir: options['scopeDir'], 203 | emit: options['emit'], 204 | files: options['files'], 205 | pretty: options['pretty'], 206 | transpileOnly: options['transpile-only'], 207 | ignore: options['ignore'] ? split(options['ignore']) : undefined, 208 | preferTsExts: options['prefer-ts-exts'], 209 | logError: options['log-error'], 210 | project: options['project'], 211 | skipProject: options['skip-project'], 212 | transpiler: options['transpiler'], 213 | skipIgnore: options['skip-ignore'], 214 | compiler: options['compiler'], 215 | compilerHost: options['compiler-host'], 216 | ignoreDiagnostics: options['ignore-diagnostics'] 217 | ? split(options['ignore-diagnostics']) 218 | : undefined, 219 | compilerOptions: parse(options['compiler-options']), 220 | }) 221 | } 222 | 223 | const compiler = { 224 | tsConfigPath, 225 | init, 226 | getCompileReqFilePath, 227 | getChildHookPath, 228 | writeReadyFile, 229 | clearErrorCompile, 230 | compileChanged: function (fileName: string) { 231 | const ext = path.extname(fileName) 232 | if (compileExtensions.indexOf(ext) < 0) return 233 | try { 234 | const code = fs.readFileSync(fileName, 'utf-8') 235 | compiler.compile({ 236 | code: code, 237 | compile: fileName, 238 | compiledPath: getCompiledPath(code, fileName, getCompiledDir()), 239 | }) 240 | } catch (e) { 241 | console.error(e) 242 | } 243 | }, 244 | compile: function (params: CompileParams) { 245 | const fileName = params.compile 246 | const code = fs.readFileSync(fileName, 'utf-8') 247 | const compiledPath = params.compiledPath 248 | 249 | // Prevent occasional duplicate compilation requests 250 | if (compiledPathsHash[compiledPath]) { 251 | return 252 | } 253 | compiledPathsHash[compiledPath] = true 254 | 255 | function writeCompiled(code: string, fileName?: string) { 256 | fs.writeFile(compiledPath, code, (err) => { 257 | err && log.error(err) 258 | fs.writeFile(compiledPath + '.done', '', (err) => { 259 | err && log.error(err) 260 | }) 261 | }) 262 | } 263 | if (fs.existsSync(compiledPath)) { 264 | return 265 | } 266 | const starTime = new Date().getTime() 267 | const m: any = { 268 | _compile: writeCompiled, 269 | } 270 | const _compile = () => { 271 | const ext = path.extname(fileName) 272 | const extHandler = require.extensions[ext]! 273 | 274 | extHandler(m, fileName) 275 | 276 | log.debug( 277 | fileName, 278 | 'compiled in', 279 | new Date().getTime() - starTime, 280 | 'ms' 281 | ) 282 | } 283 | try { 284 | _compile() 285 | } catch (e) { 286 | console.error('Compilation error in', fileName) 287 | const errorCode = 288 | 'throw ' + 289 | 'new Error(' + 290 | JSON.stringify((e as Error).message) + 291 | ')' + 292 | ';' 293 | writeCompiled(errorCode) 294 | 295 | // reinitialize ts-node compilation to clean up state after error 296 | // without timeout in causes cases error not be printed out 297 | setTimeout(() => { 298 | registerTsNode() 299 | }, 0) 300 | 301 | if (!options['error-recompile']) { 302 | return 303 | } 304 | const timeoutMs = 305 | parseInt(process.env.TS_NODE_DEV_ERROR_RECOMPILE_TIMEOUT || '0') || 306 | 5000 307 | const errorHandler = () => { 308 | clearTimeout(_errorCompileTimeout) 309 | _errorCompileTimeout = setTimeout(() => { 310 | try { 311 | _compile() 312 | restart(fileName) 313 | } catch (e) { 314 | registerTsNode() 315 | errorHandler() 316 | } 317 | }, timeoutMs) 318 | } 319 | 320 | errorHandler() 321 | } 322 | }, 323 | } 324 | 325 | return compiler 326 | } 327 | -------------------------------------------------------------------------------- /src/dedupe.ts: -------------------------------------------------------------------------------- 1 | require('dynamic-dedupe').activate(); 2 | -------------------------------------------------------------------------------- /src/get-compiled-path.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | import path from 'path' 3 | 4 | const cwd = process.cwd() 5 | 6 | export const getCompiledPath = ( 7 | code: string, 8 | fileName: string, 9 | compiledDir: string 10 | ) => { 11 | const hash = crypto 12 | .createHash('sha256') 13 | .update(fileName + code, 'utf8') 14 | .digest('hex') 15 | fileName = path.relative(cwd, fileName) 16 | const hashed = fileName.replace(/[^\w]/g, '_') + '_' + hash + '.js' 17 | return path.join(compiledDir, hashed) 18 | } 19 | -------------------------------------------------------------------------------- /src/get-cwd.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | const hasOwnProperty = (object: any, property: string) => { 4 | return Object.prototype.hasOwnProperty.call(object, property) 5 | } 6 | 7 | export const getCwd = ( 8 | dir: string, 9 | scriptMode: boolean, 10 | scriptPath?: string 11 | ) => { 12 | if (scriptMode) { 13 | if (!scriptPath) { 14 | console.error( 15 | 'Script mode must be used with a script name, e.g. `ts-node-dev -s `' 16 | ) 17 | process.exit() 18 | } 19 | 20 | if (dir) { 21 | console.error('Script mode cannot be combined with `--dir`') 22 | process.exit() 23 | } 24 | 25 | // Use node's own resolution behavior to ensure we follow symlinks. 26 | // scriptPath may omit file extension or point to a directory with or without package.json. 27 | // This happens before we are registered, so we tell node's resolver to consider ts, tsx, and jsx files. 28 | // In extremely rare cases, is is technically possible to resolve the wrong directory, 29 | // because we do not yet know preferTsExts, jsx, nor allowJs. 30 | // See also, justification why this will not happen in real-world situations: 31 | // https://github.com/TypeStrong/ts-node/pull/1009#issuecomment-613017081 32 | const exts = ['.js', '.jsx', '.ts', '.tsx'] 33 | const extsTemporarilyInstalled = [] 34 | for (const ext of exts) { 35 | if (!hasOwnProperty(require.extensions, ext)) { 36 | // tslint:disable-line 37 | extsTemporarilyInstalled.push(ext) 38 | require.extensions[ext] = function () {} // tslint:disable-line 39 | } 40 | } 41 | try { 42 | return path.dirname(require.resolve(scriptPath)) 43 | } finally { 44 | for (const ext of extsTemporarilyInstalled) { 45 | delete require.extensions[ext] // tslint:disable-line 46 | } 47 | } 48 | } 49 | 50 | return dir 51 | } 52 | -------------------------------------------------------------------------------- /src/hook.ts: -------------------------------------------------------------------------------- 1 | import vm from 'vm' 2 | import { Config } from './cfg' 3 | 4 | declare const process: NodeJS.Process & { mainModule: NodeJS.Module } 5 | 6 | export const makeHook = function ( 7 | cfg: Config, 8 | wrapper: any, 9 | callback: (file: string) => void 10 | ) { 11 | // Hook into Node's `require(...)` 12 | updateHooks() 13 | 14 | // Patch the vm module to watch files executed via one of these methods: 15 | if (cfg.vm) { 16 | patch(vm, 'createScript', 1) 17 | patch(vm, 'runInThisContext', 1) 18 | patch(vm, 'runInNewContext', 2) 19 | patch(vm, 'runInContext', 2) 20 | } 21 | 22 | /** 23 | * Patch the specified method to watch the file at the given argument 24 | * index. 25 | */ 26 | function patch(obj: any, method: string, optionsArgIndex: number) { 27 | const orig = obj[method] 28 | if (!orig) return 29 | obj[method] = function () { 30 | const opts = arguments[optionsArgIndex] 31 | let file = null 32 | if (opts) { 33 | file = typeof opts == 'string' ? opts : opts.filename 34 | } 35 | if (file) callback(file) 36 | return orig.apply(this, arguments) 37 | } 38 | } 39 | 40 | /** 41 | * (Re-)install hooks for all registered file extensions. 42 | */ 43 | function updateHooks() { 44 | Object.keys(require.extensions).forEach(function (ext) { 45 | const fn = require.extensions[ext] 46 | if (typeof fn === 'function' && fn.name !== 'nodeDevHook') { 47 | require.extensions[ext] = createHook(fn) 48 | } 49 | }) 50 | } 51 | 52 | /** 53 | * Returns a function that can be put into `require.extensions` in order to 54 | * invoke the callback when a module is required for the first time. 55 | */ 56 | function createHook(handler: (m: NodeJS.Module, filename: string) => void) { 57 | return function nodeDevHook(module: NodeJS.Module, filename: string) { 58 | if (module.parent === wrapper) { 59 | // If the main module is required conceal the wrapper 60 | module.id = '.' 61 | module.parent = null 62 | process.mainModule = module 63 | } 64 | if (!module.loaded) callback(module.filename) 65 | 66 | // Invoke the original handler 67 | handler(module, filename) 68 | 69 | // Make sure the module did not hijack the handler 70 | updateHooks() 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { fork, ChildProcess } from 'child_process' 2 | import chokidar from 'chokidar' 3 | import fs from 'fs' 4 | import readline from 'readline' 5 | 6 | const kill = require('tree-kill') 7 | 8 | import * as ipc from './ipc' 9 | import { resolveMain } from './resolveMain' 10 | import { Options } from './bin' 11 | import { makeCompiler, CompileParams } from './compiler' 12 | import { makeCfg } from './cfg' 13 | import { makeNotify } from './notify' 14 | import { makeLog } from './log' 15 | 16 | const version = require('../package.json').version 17 | const tsNodeVersion = require('ts-node').VERSION 18 | const tsVersion = require('typescript').version 19 | 20 | export const runDev = ( 21 | script: string, 22 | scriptArgs: string[], 23 | nodeArgs: string[], 24 | opts: Options 25 | ) => { 26 | if (typeof script !== 'string' || script.length === 0) { 27 | throw new TypeError('`script` must be a string') 28 | } 29 | 30 | if (!Array.isArray(scriptArgs)) { 31 | throw new TypeError('`scriptArgs` must be an array') 32 | } 33 | 34 | if (!Array.isArray(nodeArgs)) { 35 | throw new TypeError('`nodeArgs` must be an array') 36 | } 37 | 38 | // The child_process 39 | let child: 40 | | (ChildProcess & { 41 | stopping?: boolean 42 | respawn?: boolean 43 | }) 44 | | undefined 45 | 46 | const wrapper = resolveMain(__dirname + '/wrap.js') 47 | const main = resolveMain(script) 48 | const cfg = makeCfg(main, opts) 49 | const log = makeLog(cfg) 50 | const notify = makeNotify(cfg, log) 51 | 52 | // Run ./dedupe.js as preload script 53 | if (cfg.dedupe) process.env.NODE_DEV_PRELOAD = __dirname + '/dedupe' 54 | 55 | function initWatcher() { 56 | const watcher = chokidar.watch([], { 57 | usePolling: opts.poll, 58 | interval: parseInt(opts.interval) || undefined, 59 | }) 60 | watcher.on('change', restart) 61 | 62 | watcher.on('fallback', function (limit) { 63 | log.warn( 64 | 'node-dev ran out of file handles after watching %s files.', 65 | limit 66 | ) 67 | log.warn('Falling back to polling which uses more CPU.') 68 | log.info('Run ulimit -n 10000 to increase the file descriptor limit.') 69 | if (cfg.deps) log.info('... or add `--no-deps` to use less file handles.') 70 | }) 71 | return watcher 72 | } 73 | let watcher = initWatcher() 74 | 75 | let starting = false 76 | 77 | // Read for "rs" from command line 78 | if (opts.rs !== false) { 79 | const rl = readline.createInterface({ 80 | input: process.stdin, 81 | output: process.stdout, 82 | terminal: false, 83 | }) 84 | rl.on('line', (line: string) => { 85 | if (line.trim() === 'rs') { 86 | restart('', true) 87 | } 88 | }) 89 | } 90 | 91 | log.info( 92 | 'ts-node-dev ver. ' + 93 | version + 94 | ' (using ts-node ver. ' + 95 | tsNodeVersion + 96 | ', typescript ver. ' + 97 | tsVersion + 98 | ')' 99 | ) 100 | 101 | /** 102 | * Run the wrapped script. 103 | */ 104 | let compileReqWatcher: chokidar.FSWatcher 105 | function start() { 106 | if (cfg.clear) process.stdout.write('\u001bc') 107 | 108 | for (const watched of (opts.watch || '').split(',')) { 109 | if (watched) watcher.add(watched) 110 | } 111 | 112 | let cmd = nodeArgs.concat(wrapper, script, scriptArgs) 113 | const childHookPath = compiler.getChildHookPath() 114 | cmd = (opts.priorNodeArgs || []).concat(['-r', childHookPath]).concat(cmd) 115 | 116 | log.debug('Starting child process %s', cmd.join(' ')) 117 | 118 | child = fork(cmd[0], cmd.slice(1), { 119 | cwd: process.cwd(), 120 | env: process.env, 121 | }) 122 | 123 | starting = false 124 | 125 | if (compileReqWatcher) { 126 | compileReqWatcher.close() 127 | } 128 | 129 | compileReqWatcher = chokidar.watch([], { 130 | usePolling: opts.poll, 131 | interval: parseInt(opts.interval) || undefined, 132 | }) 133 | 134 | let currentCompilePath: string 135 | 136 | fs.writeFileSync(compiler.getCompileReqFilePath(), '') 137 | compileReqWatcher.add(compiler.getCompileReqFilePath()) 138 | compileReqWatcher.on('change', function (file) { 139 | fs.readFile(file, 'utf-8', function (err, data) { 140 | if (err) { 141 | log.error('Error reading compile request file', err) 142 | return 143 | } 144 | const split = data.split('\n') 145 | const compile = split[0] 146 | const compiledPath = split[1] 147 | if (currentCompilePath == compiledPath) return 148 | currentCompilePath = compiledPath 149 | 150 | if (compiledPath) { 151 | compiler.compile({ 152 | compile: compile, 153 | compiledPath: compiledPath, 154 | }) 155 | } 156 | }) 157 | }) 158 | 159 | child.on('message', function (message: CompileParams) { 160 | if ( 161 | !message.compiledPath || 162 | currentCompilePath === message.compiledPath 163 | ) { 164 | return 165 | } 166 | currentCompilePath = message.compiledPath 167 | compiler.compile(message) 168 | }) 169 | 170 | child.on('exit', function (code) { 171 | log.debug('Child exited with code %s', code) 172 | if (!child) return 173 | if (!child.respawn) process.exit(code || 0) 174 | child = undefined 175 | }) 176 | 177 | if (cfg.respawn) { 178 | child.respawn = true 179 | } 180 | 181 | if (compiler.tsConfigPath) { 182 | watcher.add(compiler.tsConfigPath) 183 | } 184 | 185 | // Listen for `required` messages and watch the required file. 186 | ipc.on(child, 'required', function (m: ipc.IPCMessage) { 187 | const required = m.required! 188 | const isIgnored = 189 | cfg.ignore.some(isPrefixOf(required)) || 190 | cfg.ignore.some(isRegExpMatch(required)) 191 | 192 | if (!isIgnored && (cfg.deps === -1 || getLevel(required) <= cfg.deps)) { 193 | log.debug(required, 'added to watcher') 194 | watcher.add(required) 195 | } 196 | }) 197 | 198 | // Upon errors, display a notification and tell the child to exit. 199 | ipc.on(child, 'error', function (m: ipc.IPCMessage) { 200 | log.debug('Child error') 201 | notify(m.error!, m.message!, 'error') 202 | stop(m.willTerminate) 203 | }) 204 | compiler.writeReadyFile() 205 | } 206 | const killChild = () => { 207 | if (!child) return 208 | log.debug('Sending SIGTERM kill to child pid', child.pid) 209 | if (opts['tree-kill']) { 210 | log.debug('Using tree-kill') 211 | kill(child.pid) 212 | } else { 213 | child.kill('SIGTERM') 214 | } 215 | } 216 | function stop(willTerminate?: boolean) { 217 | if (!child || child.stopping) { 218 | return 219 | } 220 | child.stopping = true 221 | child.respawn = true 222 | if (child.connected === undefined || child.connected === true) { 223 | log.debug('Disconnecting from child') 224 | child.disconnect() 225 | if (!willTerminate) { 226 | killChild() 227 | } 228 | } 229 | } 230 | 231 | function restart(file: string, isManualRestart?: boolean) { 232 | if (file === compiler.tsConfigPath) { 233 | notify('Reinitializing TS compilation', '') 234 | compiler.init() 235 | } 236 | compiler.clearErrorCompile() 237 | 238 | if (isManualRestart === true) { 239 | notify('Restarting', 'manual restart from user') 240 | } else { 241 | notify('Restarting', file + ' has been modified') 242 | } 243 | compiler.compileChanged(file) 244 | if (starting) { 245 | log.debug('Already starting') 246 | return 247 | } 248 | log.debug('Removing all watchers from files') 249 | //watcher.removeAll()ya 250 | 251 | watcher.close() 252 | watcher = initWatcher() 253 | starting = true 254 | if (child) { 255 | log.debug('Child is still running, restart upon exit') 256 | child.on('exit', start) 257 | stop() 258 | } else { 259 | log.debug('Child is already stopped, probably due to a previous error') 260 | start() 261 | } 262 | } 263 | 264 | // Relay SIGTERM 265 | process.on('SIGTERM', function () { 266 | log.debug('Process got SIGTERM') 267 | killChild() 268 | process.exit(0) 269 | }) 270 | 271 | const compiler = makeCompiler(opts, { 272 | restart, 273 | log: log, 274 | }) 275 | 276 | compiler.init() 277 | 278 | start() 279 | } 280 | 281 | /** 282 | * Returns the nesting-level of the given module. 283 | * Will return 0 for modules from the main package or linked modules, 284 | * a positive integer otherwise. 285 | */ 286 | function getLevel(mod: string) { 287 | const p = getPrefix(mod) 288 | return p.split('node_modules').length - 1 289 | } 290 | 291 | /** 292 | * Returns the path up to the last occurence of `node_modules` or an 293 | * empty string if the path does not contain a node_modules dir. 294 | */ 295 | function getPrefix(mod: string) { 296 | const n = 'node_modules' 297 | const i = mod.lastIndexOf(n) 298 | return ~i ? mod.slice(0, i + n.length) : '' 299 | } 300 | 301 | function isPrefixOf(value: string) { 302 | return function (prefix: string) { 303 | return value.indexOf(prefix) === 0 304 | } 305 | } 306 | 307 | function isRegExpMatch(value: string) { 308 | return function (regExp: string) { 309 | return new RegExp(regExp).test(value) 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /src/ipc.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess } from 'child_process' 2 | 3 | export type IPCMessage = { 4 | cmd?: string 5 | code?: string 6 | error?: string 7 | message?: string 8 | willTerminate?: boolean 9 | required?: string 10 | } 11 | 12 | /** 13 | * Checks if the given message is an internal node-dev message. 14 | */ 15 | function isNodeDevMessage(m: IPCMessage) { 16 | return m.cmd === 'NODE_DEV' 17 | } 18 | 19 | /** 20 | * Sends a message to the given process. 21 | */ 22 | export const send = function (m: IPCMessage, dest: NodeJS.Process = process) { 23 | m.cmd = 'NODE_DEV' 24 | if (dest.send) dest.send(m) 25 | } 26 | 27 | export const on = function ( 28 | proc: ChildProcess, 29 | type: string, 30 | cb: (m: IPCMessage) => void 31 | ) { 32 | function handleMessage(m: IPCMessage) { 33 | if (isNodeDevMessage(m) && type in m) cb(m) 34 | } 35 | proc.on('internalMessage', handleMessage) 36 | proc.on('message', handleMessage) 37 | } 38 | 39 | export const relay = function ( 40 | src: ChildProcess, 41 | dest: NodeJS.Process = process 42 | ) { 43 | function relayMessage(m: IPCMessage) { 44 | if (isNodeDevMessage(m)) dest.send!(m) 45 | } 46 | src.on('internalMessage', relayMessage) 47 | src.on('message', relayMessage) 48 | } 49 | -------------------------------------------------------------------------------- /src/log.ts: -------------------------------------------------------------------------------- 1 | const util = require('util') 2 | import { Config } from './cfg' 3 | 4 | const colors = { 5 | info: '36', 6 | error: '31;1', 7 | warn: '33', 8 | debug: '90', 9 | } 10 | 11 | type LogFn = (msg: string, level: string) => void 12 | 13 | export type Log = LogFn & { 14 | error: (...p: any[]) => void 15 | warn: (...p: any[]) => void 16 | info: (...p: any[]) => void 17 | debug: (...p: any[]) => void 18 | } 19 | 20 | type LogLevel = keyof typeof colors 21 | 22 | const formatDate = (date: Date) => { 23 | return [date.getHours(), date.getMinutes(), date.getSeconds()] 24 | .map((n) => n.toString().padStart(2, '0')) 25 | .join(':') 26 | } 27 | 28 | /** 29 | * Logs a message to the console. The level is displayed in ANSI colors, 30 | * either bright red in case of an error or green otherwise. 31 | */ 32 | export const makeLog = function (cfg: Config) { 33 | function log(msg: string, level: LogLevel) { 34 | if (cfg.quiet && level === 'info') return 35 | if (cfg.timestamp) msg = color(formatDate(new Date()), '30;1') + ' ' + msg 36 | const c = colors[level.toLowerCase() as LogLevel] || '32' 37 | console.log('[' + color(level.toUpperCase(), c) + '] ' + msg) 38 | } 39 | 40 | function color(s: string, c: string) { 41 | if (process.stdout.isTTY) { 42 | return '\x1B[' + c + 'm' + s + '\x1B[0m' 43 | } 44 | return s 45 | } 46 | 47 | log.debug = function () { 48 | if (!cfg.debug) return 49 | log(util.format.apply(util, arguments), 'debug') 50 | } 51 | log.info = function () { 52 | log(util.format.apply(util, arguments), 'info') 53 | } 54 | 55 | log.warn = function () { 56 | log(util.format.apply(util, arguments), 'warn') 57 | } 58 | 59 | log.error = function () { 60 | log(util.format.apply(util, arguments), 'error') 61 | } 62 | 63 | return log as Log 64 | } 65 | -------------------------------------------------------------------------------- /src/notify.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { Config } from './cfg' 3 | import { Log } from './log' 4 | let notifier: any = null 5 | try { 6 | notifier = require('node-notifier') 7 | } catch (error) { 8 | notifier = null 9 | } 10 | 11 | function icon(level: string) { 12 | return path.resolve(__dirname, '../icons/node_' + level + '.png') 13 | } 14 | 15 | /** 16 | * Displays a desktop notification and writes a message to the console. 17 | */ 18 | export const makeNotify = function (cfg: Config, log: Log) { 19 | return function (title: string, msg: string, level?: string) { 20 | level = level || 'info' 21 | log([title, msg].filter((_) => _).join(': '), level) 22 | if (notifier !== null && cfg.notify) { 23 | notifier.notify({ 24 | title: title || 'node.js', 25 | icon: icon(level), 26 | message: msg, 27 | }) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/resolveMain.ts: -------------------------------------------------------------------------------- 1 | const resolve = require('resolve') 2 | 3 | type PNPVersions = NodeJS.ProcessVersions & { pnp: boolean } 4 | 5 | function resolveRequest(req: string) { 6 | // The `resolve` package is prebuilt through ncc, which prevents 7 | // PnP from being able to inject itself into it. To circumvent 8 | // this, we simply use PnP directly when available. 9 | 10 | if ((process.versions as PNPVersions).pnp) { 11 | const { resolveRequest } = require(`pnpapi`) 12 | return resolveRequest(req, process.cwd() + '/') 13 | } else { 14 | const opts = { 15 | basedir: process.cwd(), 16 | paths: [process.cwd()], 17 | } 18 | return resolve.sync(req, opts) 19 | } 20 | } 21 | 22 | export const resolveMain = function (main: string) { 23 | try { 24 | return resolveRequest(main + '.ts') 25 | } catch (e) { 26 | try { 27 | return resolveRequest(main + '/index.ts') 28 | } catch (e) { 29 | return resolveRequest(main) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/wrap.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | const childProcess = require('child_process') 3 | import { fork, ForkOptions } from 'child_process' 4 | const resolve = require('resolve').sync 5 | import { makeHook } from './hook' 6 | import * as ipc from './ipc' 7 | import { resolveMain } from './resolveMain' 8 | import { makeCfg } from './cfg' 9 | // const Module = require('module') 10 | // Remove wrap.js from the argv array 11 | 12 | process.argv.splice(1, 1) 13 | 14 | // Resolve the location of the main script relative to cwd 15 | const main = resolveMain(process.argv[1]) 16 | 17 | const cfg = makeCfg(main, {}) 18 | 19 | if (process.env.TS_NODE_DEV === undefined) { 20 | process.env.TS_NODE_DEV = 'true' 21 | } 22 | 23 | if (process.env.NODE_DEV_PRELOAD) { 24 | require(process.env.NODE_DEV_PRELOAD) 25 | } 26 | 27 | // Listen SIGTERM and exit unless there is another listener 28 | process.on('SIGTERM', function () { 29 | if (process.listeners('SIGTERM').length === 1) process.exit(0) 30 | }) 31 | 32 | if (cfg.fork) { 33 | const oldFork = fork 34 | // Overwrite child_process.fork() so that we can hook into forked processes 35 | // too. We also need to relay messages about required files to the parent. 36 | const newFork = function ( 37 | modulePath: string, 38 | args: string[], 39 | options: ForkOptions 40 | ) { 41 | const child = oldFork(__filename, [modulePath].concat(args), options) 42 | ipc.relay(child) 43 | return child 44 | } 45 | childProcess.fork = newFork 46 | } 47 | 48 | // const lastRequired = null 49 | // const origRequire = Module.prototype.require 50 | // Module.prototype.require = function (requirePath) { 51 | // lastRequired = { path: requirePath, filename: this.filename } 52 | // return origRequire.apply(this, arguments) 53 | // } 54 | 55 | // Error handler that displays a notification and logs the stack to stderr: 56 | let caught = false 57 | process.on('uncaughtException', function (err: any) { 58 | // NB: err can be null 59 | // Handle exception only once 60 | if (caught) return 61 | caught = true 62 | // If there's a custom uncaughtException handler expect it to terminate 63 | // the process. 64 | const hasCustomHandler = process.listeners('uncaughtException').length > 1 65 | const isTsError = err && err.message && /TypeScript/.test(err.message) 66 | if (!hasCustomHandler && !isTsError) { 67 | console.error((err && err.stack) || err) 68 | } 69 | 70 | ipc.send({ 71 | error: isTsError ? '' : (err && err.name) || 'Error', 72 | // lastRequired: lastRequired, 73 | message: err ? err.message : '', 74 | code: err && err.code, 75 | willTerminate: hasCustomHandler, 76 | }) 77 | }) 78 | 79 | // Hook into require() and notify the parent process about required files 80 | makeHook(cfg, module, function (file) { 81 | ipc.send({ required: file }) 82 | }) 83 | 84 | // Check if a module is registered for this extension 85 | // const ext = path.extname(main).slice(1) 86 | // const mod = cfg.extensions[ext] 87 | 88 | // // Support extensions where 'require' returns a function that accepts options 89 | // if (typeof mod == 'object' && mod.name) { 90 | // const fn = require(resolve(mod.name, { basedir: path.dirname(main) })) 91 | // if (typeof fn == 'function' && mod.options) { 92 | // // require returned a function, call it with options 93 | // fn(mod.options) 94 | // } 95 | // } else if (typeof mod == 'string') { 96 | // require(resolve(mod, { basedir: path.dirname(main) })) 97 | // } 98 | 99 | // Execute the wrapped script 100 | require(main) 101 | -------------------------------------------------------------------------------- /test/fixture/.rcfile: -------------------------------------------------------------------------------- 1 | console.log('NO EXT') 2 | 3 | exports.x = 1 -------------------------------------------------------------------------------- /test/fixture/add-req.ts: -------------------------------------------------------------------------------- 1 | console.log('added --require') 2 | -------------------------------------------------------------------------------- /test/fixture/dep-ts-error.ts: -------------------------------------------------------------------------------- 1 | export const fn = (x: number) => { 2 | return 'v1' 3 | } 4 | 5 | -------------------------------------------------------------------------------- /test/fixture/dep.ts: -------------------------------------------------------------------------------- 1 | export const fn = (x: number) => { 2 | return 'v1' 3 | } 4 | 5 | -------------------------------------------------------------------------------- /test/fixture/dir-test/imported.js: -------------------------------------------------------------------------------- 1 | module.exports.hello = 'world' 2 | ; 3 | -------------------------------------------------------------------------------- /test/fixture/dir-test/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import values from './imported' 3 | 4 | console.log(values) -------------------------------------------------------------------------------- /test/fixture/dir-test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "include": ["./*.ts", "./*.js"], 4 | "compilerOptions": { 5 | "allowJs": true, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true 8 | } 9 | } -------------------------------------------------------------------------------- /test/fixture/file.json: -------------------------------------------------------------------------------- 1 | { 2 | "file": "json" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixture/folder/some-file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wclr/ts-node-dev/32bdc92458a59f66bcbd36e87de2a793f529a825/test/fixture/folder/some-file -------------------------------------------------------------------------------- /test/fixture/import-json.ts: -------------------------------------------------------------------------------- 1 | import data from './file.json' 2 | 3 | console.log('JSON DATA:', data) -------------------------------------------------------------------------------- /test/fixture/js-module.js: -------------------------------------------------------------------------------- 1 | import { fn } from './dep' 2 | 3 | console.log(fn(1)) 4 | 5 | require('./.rcfile') 6 | 7 | const json = require('./file.json') 8 | 9 | console.log('json',json) 10 | console.log('JS MODULE') 11 | -------------------------------------------------------------------------------- /test/fixture/nameof.ts: -------------------------------------------------------------------------------- 1 | console.log(nameof(console)) -------------------------------------------------------------------------------- /test/fixture/node_modules/package/index.js: -------------------------------------------------------------------------------- 1 | require('level2-package') 2 | console.log('PACKAGE FROM NODE_MODULES') -------------------------------------------------------------------------------- /test/fixture/node_modules/package/node_modules/level2-package/index.js: -------------------------------------------------------------------------------- 1 | console.log('PACKAGE FROM LEVEL2 NODE_MODULES') -------------------------------------------------------------------------------- /test/fixture/not-found/js-with-not-found.js: -------------------------------------------------------------------------------- 1 | require('./not-found-js') -------------------------------------------------------------------------------- /test/fixture/not-found/with-not-found-js.ts: -------------------------------------------------------------------------------- 1 | import { fn } from './not-found-js' 2 | 3 | console.log(fn(1)) 4 | -------------------------------------------------------------------------------- /test/fixture/prefer/prefer-dep.js: -------------------------------------------------------------------------------- 1 | console.log('PREFER DEP JS') 2 | -------------------------------------------------------------------------------- /test/fixture/prefer/prefer-dep.ts: -------------------------------------------------------------------------------- 1 | console.log('PREFER DEP TS') -------------------------------------------------------------------------------- /test/fixture/prefer/prefer.js: -------------------------------------------------------------------------------- 1 | require('./prefer-dep') 2 | console.log('PREFER JS') -------------------------------------------------------------------------------- /test/fixture/prefer/prefer.ts: -------------------------------------------------------------------------------- 1 | import './prefer-dep' 2 | console.log('PREFER TS') -------------------------------------------------------------------------------- /test/fixture/req-package.ts: -------------------------------------------------------------------------------- 1 | require('package') -------------------------------------------------------------------------------- /test/fixture/simple.ts: -------------------------------------------------------------------------------- 1 | import { fn } from './dep' 2 | 3 | console.log(fn(1)) 4 | -------------------------------------------------------------------------------- /test/fixture/to-transform.ts: -------------------------------------------------------------------------------- 1 | console.log("something") -------------------------------------------------------------------------------- /test/fixture/uncaught-handler.ts: -------------------------------------------------------------------------------- 1 | process.on('uncaughtException', function (e) { 2 | setTimeout(function () { 3 | console.log('async', e); 4 | }, 100); 5 | }); 6 | 7 | //const foo = () => {console.log('s')} 8 | 9 | // eslint-disable-next-line no-undef 10 | foo(); // undefined / throws exception -------------------------------------------------------------------------------- /test/fixture/with-error.ts: -------------------------------------------------------------------------------- 1 | import { fn } from './dep-ts-error' 2 | 3 | console.log(fn('1')) 4 | -------------------------------------------------------------------------------- /test/fixture/with-not-found.ts: -------------------------------------------------------------------------------- 1 | import { fn } from './not-found' 2 | 3 | console.log(fn(1)) 4 | -------------------------------------------------------------------------------- /test/manual/add-require-2.ts: -------------------------------------------------------------------------------- 1 | console.log('second node --require') 2 | -------------------------------------------------------------------------------- /test/manual/add-require.js: -------------------------------------------------------------------------------- 1 | console.log('additional node --require') -------------------------------------------------------------------------------- /test/manual/dep-interface.ts: -------------------------------------------------------------------------------- 1 | export interface A { 2 | //a: string 3 | } 4 | -------------------------------------------------------------------------------- /test/manual/dep.ts: -------------------------------------------------------------------------------- 1 | 2 | export const fn = (x: number) => { 3 | console.log('function from dep module here') 4 | } 5 | -------------------------------------------------------------------------------- /test/manual/run.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process' 2 | 3 | const cmd = [ 4 | 'node lib/bin', 5 | '--exit-child', 6 | '--tree-kill', 7 | '--clear', 8 | '-r tsconfig-paths/register', 9 | '-r ./test/manual/add-require', 10 | '-r ./test/manual/add-require-2', 11 | '-r esm', 12 | '-O "{\\"module\\": \\"es6\\"}"', 13 | '--preserve-symlinks', 14 | '--respawn', 15 | '--ignore-watch', 16 | 'lib', 17 | '--ignore-watch bin', 18 | '--prefer-ts', 19 | '--debug', 20 | '--poll', 21 | '--interval 1000', 22 | '--inspect', 23 | '-- test/manual/test-script', 24 | 'test-arg', 25 | '--fd', 26 | ].join(' ') 27 | 28 | execSync(cmd, { stdio: 'inherit' }) 29 | -------------------------------------------------------------------------------- /test/manual/test-script.ts: -------------------------------------------------------------------------------- 1 | import { fn } from './dep' 2 | import { A } from './dep-interface' 3 | const chalk = require('chalk') 4 | const str: string = process.argv[2] 5 | const obj: A = { 6 | a: '1', 7 | b: 2 8 | } 9 | 10 | fn(1) 11 | 12 | console.log('test', str) 13 | 14 | 15 | setInterval(() => { 16 | console.log(chalk.green('Working')) 17 | }, 5000) 18 | 19 | 20 | console.log('test', str) 21 | setTimeout(() => { 22 | throw new Error('fds') 23 | }, 1000) 24 | 25 | -------------------------------------------------------------------------------- /test/spawn.ts: -------------------------------------------------------------------------------- 1 | import child from 'child_process' 2 | import path from 'path' 3 | const bin = path.join(__dirname, '/../lib/bin') 4 | 5 | export const tmpDir = path.join(__dirname, '../.tmp') 6 | export const scriptsDir = path.join(tmpDir, 'fixture') 7 | 8 | let outputTurnedOn = false 9 | 10 | export const turnOnOutput = () => { 11 | outputTurnedOn = true 12 | } 13 | 14 | export const spawnTsNodeDev = ( 15 | cmd: string, 16 | options: { stdout?: boolean; stderr?: boolean; env?: any } = {} 17 | ) => { 18 | const opts = { ...options } 19 | const nodeArg = [bin].concat(cmd.split(' ')) 20 | const ps = child.spawn('node', nodeArg, { 21 | cwd: scriptsDir, 22 | env: { 23 | ...process.env, 24 | ...opts.env, 25 | }, 26 | }) 27 | let out = '' 28 | let err = '' 29 | 30 | ps.stderr.on('data', function (data) { 31 | if (opts.stderr || outputTurnedOn) { 32 | // eslint-disable-next-line no-console 33 | console.log('STDERR:', data.toString().replace(/\n$/, '')) 34 | } 35 | err += data.toString() 36 | }) 37 | ps.stdout.on('data', function (data) { 38 | if (opts.stdout || outputTurnedOn) { 39 | // eslint-disable-next-line no-console 40 | console.log('STDOUT:', data.toString().replace(/\n$/, '')) 41 | } 42 | out += data.toString() 43 | }) 44 | ps.on('disconnect', () => { 45 | // eslint-disable-next-line no-console 46 | console.log('im out') 47 | }) 48 | const testPattern = (pattern: string | RegExp, str: string) => { 49 | return typeof pattern === 'string' 50 | ? str.indexOf(pattern) >= 0 51 | : pattern.test(str) 52 | } 53 | const self = { 54 | ps, 55 | turnOnOutput: () => { 56 | opts.stderr = true 57 | opts.stdout = true 58 | return self 59 | }, 60 | getStdout: () => out, 61 | getStderr: () => err, 62 | waitForLine: (pattern: string | RegExp) => { 63 | return new Promise((resolve) => { 64 | const listener = (data: string) => { 65 | const line = data.toString() 66 | if (testPattern(pattern, line)) { 67 | ps.stdout.removeListener('data', listener) 68 | resolve(line) 69 | } 70 | } 71 | ps.stdout.on('data', listener) 72 | }) 73 | }, 74 | waitForErrorLine: (pattern: string | RegExp) => { 75 | return new Promise((resolve) => { 76 | const listener = (data: string) => { 77 | const line = data.toString() 78 | if (testPattern(pattern, line)) { 79 | ps.stderr.removeListener('data', listener) 80 | resolve(line) 81 | } 82 | } 83 | ps.stderr.on('data', listener) 84 | }) 85 | }, 86 | exit: () => { 87 | return new Promise((resolve) => { 88 | ps.stdout.removeAllListeners('data') 89 | ps.stderr.removeAllListeners('data') 90 | ps.removeAllListeners('exit') 91 | ps.on('exit', function (code) { 92 | resolve(code) 93 | }) 94 | ps.kill() 95 | }) 96 | }, 97 | } 98 | return self 99 | } 100 | -------------------------------------------------------------------------------- /test/transformer.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | export default function (program: ts.Program, pluginOptions: {}) { 3 | return (ctx: ts.TransformationContext) => { 4 | return (sourceFile: ts.SourceFile) => { 5 | function visitor(node: ts.Node): ts.Node { 6 | if (ts.isStringLiteral(node)) { 7 | return ts.createLiteral('transformed'); 8 | } 9 | return ts.visitEachChild(node, visitor, ctx) 10 | } 11 | return ts.visitEachChild(sourceFile, visitor, ctx) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/tsnd.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-misused-promises */ 2 | import { describe, it } from 'mocha' 3 | import chai from 'chai' 4 | import { spawnTsNodeDev, scriptsDir, tmpDir, turnOnOutput } from './spawn' 5 | import fs from 'fs-extra' 6 | import { join } from 'path' 7 | import touch = require('touch') 8 | 9 | if (process.argv.slice(2).includes('--output')) { 10 | turnOnOutput() 11 | } 12 | 13 | const { assert: t } = chai 14 | 15 | export const replaceText = async ( 16 | script: string, 17 | pattern: string | RegExp, 18 | replace: string 19 | ) => { 20 | const textFile = join(scriptsDir, script) 21 | const text = await fs.readFile(textFile, 'utf-8') 22 | return fs.writeFile(textFile, text.replace(pattern, replace)) 23 | } 24 | 25 | export const writeFile = async (script: string, text: string) => { 26 | const textFile = join(scriptsDir, script) 27 | return fs.writeFile(textFile, text) 28 | } 29 | 30 | export const removeFile = async (script: string) => { 31 | const textFile = join(scriptsDir, script) 32 | return fs.remove(textFile) 33 | } 34 | 35 | const waitFor = (timeout: number) => { 36 | return new Promise((resolve) => setTimeout(resolve, timeout)) 37 | } 38 | 39 | fs.ensureDirSync(tmpDir) 40 | fs.removeSync(join(tmpDir, 'fixture')) 41 | fs.copySync(join(__dirname, 'fixture'), scriptsDir) 42 | describe('ts-node-dev', function () { 43 | this.timeout(5000) 44 | it('should restart on file change', async () => { 45 | const ps = spawnTsNodeDev('--respawn --poll simple.ts') 46 | await ps.waitForLine(/v1/) 47 | setTimeout(() => replaceText('dep.ts', 'v1', 'v2'), 250) 48 | await ps.waitForLine(/v2/) 49 | t.ok(true, 'Changed code version applied.') 50 | await ps.exit() 51 | // revert 52 | await replaceText('dep.ts', 'v2', 'v1') 53 | }) 54 | 55 | it('allow watch arbitrary folder/file', async () => { 56 | const ps = spawnTsNodeDev('--respawn --watch folder,folder2 simple.ts') 57 | await ps.waitForLine(/v1/) 58 | setTimeout(() => touch(join(scriptsDir, 'folder/some-file')), 250) 59 | await ps.waitForLine(/Restarting.*some-file/) 60 | t.ok(true, 'works') 61 | await ps.exit() 62 | }) 63 | 64 | it('should report an error on start', async () => { 65 | const ps = spawnTsNodeDev('--respawn with-error.ts') 66 | await ps.waitForLine(/\[ERROR\]/) 67 | const out = ps.getStdout() 68 | const err = ps.getStderr() 69 | 70 | t.ok(/Compilation error in/.test(err), 'Reports error file') 71 | t.ok(/[ERROR].*Unable to compile TypeScript/.test(out), 'Report TS error') 72 | t.ok(/Argument of type/.test(out), 'Report TS error diagnostics') 73 | 74 | setTimeout(() => replaceText('with-error.ts', `'1'`, '1'), 250) 75 | 76 | // PROBLEM: if we try to fix not required/compiled dep it does not work. 77 | // setTimeout(() => replaceText('dep-ts-error.ts', 'number', 'string'), 250) 78 | 79 | await ps.waitForLine(/v1/) 80 | t.ok(true, 'Restarted successfully after error fixed.') 81 | await ps.exit() 82 | await replaceText('with-error.ts', '1', `'1'`) 83 | }) 84 | 85 | it('should not output INFO messages with --quiet', async () => { 86 | const ps = spawnTsNodeDev('--respawn --poll --quiet simple.ts') 87 | await ps.waitForLine(/v1/) 88 | setTimeout(() => replaceText('dep.ts', 'v1', 'v2'), 250) 89 | await ps.waitForLine(/v2/) 90 | 91 | await ps.exit() 92 | 93 | t.equal(ps.getStdout(), ['v1', 'v2', ''].join('\n')) 94 | 95 | await replaceText('dep.ts', 'v2', 'v1') 96 | }) 97 | 98 | it('should report an error with --log-error and continue to work', async () => { 99 | const ps = spawnTsNodeDev('--respawn --log-error with-error.ts') 100 | await ps.waitForErrorLine(/error/) 101 | 102 | const out = ps.getStderr() 103 | t.ok(/error.*Argument of type/.test(out), 'Reports error in stderr') 104 | 105 | setTimeout(() => replaceText('with-error.ts', `'1'`, '1'), 250) 106 | 107 | await ps.waitForLine(/Restarting:/) 108 | await ps.waitForLine(/v1/) 109 | t.ok(true, 'Restarted successfully after error fixed.') 110 | await ps.exit() 111 | await replaceText('with-error.ts', '1', `'1'`) 112 | }) 113 | 114 | it('should restart on adding not imported module', async () => { 115 | const ps = spawnTsNodeDev('--respawn --error-recompile with-error.ts', { 116 | env: { 117 | TS_NODE_DEV_ERROR_RECOMPILE_TIMEOUT: 50, 118 | }, 119 | }) 120 | await ps.waitForLine(/[ERROR]/) 121 | 122 | setTimeout(() => replaceText('dep-ts-error.ts', 'number', 'string'), 250) 123 | 124 | await ps.waitForLine(/v1/) 125 | t.ok(true, 'Restarted successfully after error fixed.') 126 | await ps.exit() 127 | await replaceText('dep-ts-error.ts', 'string', 'number') 128 | }) 129 | 130 | it('should recompile module on error and restarts', async () => { 131 | const notFoundSource = `export const fn = (x: number) => { 132 | return 'v1' 133 | } 134 | ` 135 | const ps = spawnTsNodeDev('--respawn --error-recompile with-not-found.ts', { 136 | env: { 137 | TS_NODE_DEV_ERROR_RECOMPILE_TIMEOUT: 20, 138 | }, 139 | }) 140 | await ps.waitForLine(/[ERROR]/) 141 | 142 | setTimeout(() => writeFile('not-found.ts', notFoundSource), 250) 143 | 144 | await ps.waitForLine(/v1/) 145 | t.ok(true, 'Restarted successfully after module was created.') 146 | await ps.exit() 147 | await removeFile('not-found.ts') 148 | }) 149 | 150 | it('should handle allowJs option and compile JS modules', async () => { 151 | const cOptions = { allowJs: true, esModuleInterop: false } 152 | const ps = spawnTsNodeDev( 153 | [ 154 | `--respawn`, 155 | `--compiler-options=${JSON.stringify(cOptions)}`, 156 | `js-module.js`, 157 | ].join(' ') 158 | ) 159 | await ps.waitForLine(/JS MODULE/) 160 | t.ok(true, 'ok') 161 | await ps.exit() 162 | }) 163 | 164 | it('should handle -r esm option and load JS modules', async () => { 165 | const ps = spawnTsNodeDev([`--respawn`, `-r esm`, `js-module.js`].join(' ')) 166 | await ps.waitForLine(/JS MODULE/) 167 | t.ok(true, 'ok') 168 | await ps.exit() 169 | }) 170 | 171 | it('should handle resolveJsonModule option and load JSON modules', async () => { 172 | const cOptions = { resolveJsonModule: true } 173 | const ps = spawnTsNodeDev( 174 | [ 175 | `--respawn`, 176 | `--compiler ttypescript`, 177 | `--compiler-options=${JSON.stringify(cOptions)}`, 178 | `import-json`, 179 | ].join(' ') 180 | ) 181 | await ps.waitForLine(/JSON DATA: { file: 'json' }/) 182 | t.ok(true, 'ok') 183 | await ps.exit() 184 | }) 185 | 186 | describe('--dir and --script-mode flags', () => { 187 | it('should not allow --script-mode and --dir together', async () => { 188 | const ps = spawnTsNodeDev( 189 | [`--script-mode`, `--dir folder`, `simple.ts`].join(' ') 190 | ) 191 | await ps.waitForErrorLine(/Script mode cannot be combined with `--dir`/) 192 | t.ok(true, 'ok') 193 | await ps.exit() 194 | }) 195 | 196 | it('should use the tsconfig at --dir when defined', async () => { 197 | const ps = spawnTsNodeDev( 198 | [`--dir dir-test`, `dir-test/index.ts`].join(' ') 199 | ) 200 | await ps.waitForLine(/\{ hello: 'world' \}/) 201 | t.ok(true, 'ok') 202 | await ps.exit() 203 | }) 204 | 205 | it('should use the tsconfig at --script-mode when defined', async () => { 206 | const ps = spawnTsNodeDev([`-s`, `dir-test/index.ts`].join(' ')) 207 | await ps.waitForLine(/\{ hello: 'world' \}/) 208 | t.ok(true, 'ok') 209 | await ps.exit() 210 | }) 211 | 212 | it('should fail if not using --dir or --script-mode on dir-test/index.ts', async () => { 213 | const cOptions = { allowJs: true, esModuleInterop: false } 214 | const ps = spawnTsNodeDev( 215 | [ 216 | `--compiler-options=${JSON.stringify(cOptions)}`, 217 | `dir-test/index.ts`, 218 | ].join(' ') 219 | ) 220 | await ps.waitForLine(/has no default export./) 221 | t.ok(true, 'ok') 222 | await ps.exit() 223 | }) 224 | }) 225 | 226 | it('should allow to use custom TS transformers', async () => { 227 | const cOptions = { plugins: [{ transform: 'ts-nameof', type: 'raw' }] } 228 | const ps = spawnTsNodeDev( 229 | [ 230 | `--respawn`, 231 | `--compiler ttypescript`, 232 | `--compiler-options=${JSON.stringify(cOptions)}`, 233 | `nameof.ts`, 234 | ].join(' ') 235 | ) 236 | await ps.waitForLine(/console/) 237 | 238 | await ps.exit() 239 | }) 240 | 241 | it('It allows to use custom TS Transformers', async () => { 242 | const cOptions = { plugins: [{ transform: __dirname + '/transformer.ts' }] } 243 | const ps = spawnTsNodeDev( 244 | [ 245 | `--respawn`, 246 | `--compiler ttypescript`, 247 | `--compiler-options=${JSON.stringify(cOptions)}`, 248 | `to-transform.ts`, 249 | ].join(' ') 250 | ) 251 | await ps.waitForLine(/transformed/) 252 | t.ok(true, 'ok') 253 | await ps.exit() 254 | }) 255 | 256 | describe('--prefer-ts-exts flag', async () => { 257 | it('should require existing JS modules by default', async () => { 258 | const ps = spawnTsNodeDev([`--respawn`, `prefer/prefer`].join(' ')) 259 | await ps.waitForLine(/PREFER DEP JS/) 260 | await ps.waitForLine(/PREFER TS/) 261 | await ps.exit() 262 | t.ok(true) 263 | }) 264 | 265 | it('should require TS modules with --ts-prefer-exts', async () => { 266 | const ps = spawnTsNodeDev( 267 | [`--respawn`, `--prefer-ts-exts`, `prefer/prefer`].join(' ') 268 | ) 269 | await ps.waitForLine(/PREFER DEP TS/) 270 | await ps.waitForLine(/PREFER TS/) 271 | 272 | setTimeout( 273 | () => replaceText('prefer/prefer-dep.ts', 'DEP', 'DEP MOD'), 274 | 250 275 | ) 276 | 277 | await ps.waitForLine(/PREFER DEP MOD TS/) 278 | 279 | await ps.exit() 280 | t.ok(true) 281 | await replaceText('prefer/prefer-dep.ts', 'DEP MOD', 'DEP') 282 | }) 283 | }) 284 | // watching required with -r not implemented 285 | it.skip('should add require with -r flag', async () => { 286 | const ps = spawnTsNodeDev( 287 | [ 288 | `-r ./add-req`, 289 | //`--debug`, 290 | `simple`, 291 | ].join(' ') 292 | ) 293 | await ps.waitForLine(/added --require/) 294 | await ps.waitForLine(/v1/) 295 | 296 | //setTimeout(() => replaceText('add-req', 'added', 'changed'), 250) 297 | //await ps.exit() 298 | // 299 | await ps.waitForLine(/changed --require/) 300 | t.ok(true) 301 | }) 302 | 303 | it('should handle --deps flag', async () => { 304 | const ps = spawnTsNodeDev([`--deps`, `--respawn`, `req-package`].join(' ')) 305 | 306 | await ps.waitForLine(/PACKAGE/) 307 | 308 | setTimeout( 309 | () => 310 | replaceText( 311 | 'node_modules/package/index.js', 312 | 'PACKAGE', 313 | 'CHANGED PACKAGE' 314 | ), 315 | 100 316 | ) 317 | 318 | await ps.waitForLine(/CHANGED PACKAGE/) 319 | await ps.exit() 320 | t.ok(true) 321 | }) 322 | 323 | it('should handle deep deps with --deps flag', async () => { 324 | const ps = spawnTsNodeDev( 325 | [`--all-deps`, `--respawn`, `req-package`].join(' ') 326 | ) 327 | 328 | await ps.waitForLine(/PACKAGE/) 329 | 330 | setTimeout( 331 | () => 332 | replaceText( 333 | 'node_modules/package/node_modules/level2-package/index.js', 334 | 'PACKAGE', 335 | 'CHANGED PACKAGE' 336 | ), 337 | 100 338 | ) 339 | 340 | await ps.waitForLine(/CHANGED PACKAGE/) 341 | await ps.exit() 342 | t.ok(true) 343 | }) 344 | 345 | it.skip('should error on wrong cli flag', async () => { 346 | const ps = spawnTsNodeDev([`--transpileOnly`, `req-package`].join(' ')) 347 | 348 | await ps.waitForLine(/bad option/) 349 | 350 | await ps.waitForLine(/CHANGED PACKAGE/) 351 | await ps.exit() 352 | t.ok(true) 353 | }) 354 | 355 | it('should put compiled sources in custom --cache-directory', async () => { 356 | const cacheDir = join(tmpDir, 'test-cached-dir') 357 | fs.removeSync(cacheDir) 358 | const ps = spawnTsNodeDev(`--cache-directory ${cacheDir} simple.ts`) 359 | await ps.waitForLine(/v1/) 360 | await ps.exit() 361 | const list = fs.readdirSync(cacheDir) 362 | t.ok(list[0] === 'compiled', '"compiled" dir is there') 363 | }) 364 | }) 365 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "outDir": "./lib", 5 | "noEmit": false 6 | }, 7 | "include": ["src/**/*"] 8 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["es2017"], 5 | "module": "commonjs", 6 | "declaration": false, 7 | "skipLibCheck": true, 8 | "sourceMap": false, 9 | "strict": true, 10 | "noEmit": true, 11 | "resolveJsonModule": true, 12 | "esModuleInterop": true, 13 | "newLine": "LF" 14 | } 15 | } 16 | --------------------------------------------------------------------------------