TypeScript
TypeScript copied to clipboard
Typescript Watch - Cleaning Target Files on Source Deletion
Can we add a compiler option to also glob-delete a target file when the source file is removed? We run into this constantly and have to gulp clean frequently - it makes the usage of tsc -watch pointless as we have to constantly purge the output directory.
For tsc --watch we have a list of output files from the previous iteration, we just need to diff that with the current list, and delete the old outputs.
Is there a recommended way to work around this today? We have prepended rimraf lib/ to our build scripts, which removes the folder containing the compiled JS files.
But this is caused problems with sym-linked modules. The importing project's watch then gets broken with TS2307: Cannot find module, as it only seem to register the disappearance of the lib folder, but not the new one put in its place.
Can someone provide some pointers where I can help implement this? @mhegazy?
Things have gotten a bit more complicated with the addition of the Builder the and the WatchProgram (see https://github.com/Microsoft/TypeScript/pull/20234), there is not really a place to keep this information, since it is always partial, and we are not rebuilding the information after every change as we used to do. I suppose we could investigate restarting the builder if a file was removed. but we will need a way to detect that in the first place..
Hi!
This is constantly biting us during development. A file is removed - but nightwatch / some other library still picks it up and runs tests based on code that should not be there.
Last activity on this was more than 8 months ago, but I hope there is still a possibility for this to be added?
Thanks!
If you're using jest, you can use ts-jest
My 'solution' is obviously not the full thing that is trying to be implemented right now, it's just some very basic behavior I need myself, But I think it might help some people that need something like this right now. So if it's alright, I will just leave a link here for now: https://www.npmjs.com/package/ts-cleaner
This functionality would also be helpful on a simple --clean, this is effecting us in CI and when switching between branches locally
ts-cleaner didn't work for me, so I wrote ts-purify. It works exactly the same way as ts-cleaner but uses fb-watchman (its only dependency) instead of chokidar. This makes it very small and very efficient at watching, but on the other hand, you'll need to install watchman to use it.
Hello!!
I solved this issue with watch, diff-file-tree and rimraf:
tsconfig.json
{
"compileOnSave": false,
"compilerOptions": {
"rootDir": "./src",
"outDir": "./.tmp",
"preserveSymlinks": true,
"sourceMap": true,
"declaration": true,
"module": "umd",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"typeRoots": [
"node_modules/@types"
],
"lib": [
"es6"
]
},
"include": [
"./src/**/*.*"
]
}
package.json
{
"name": "diff-tree",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "tsc && npm run build:copy",
"build:copy": "node -e \"(d = require('diff-file-tree')).diff('./.tmp', './dist').then((ch)=>d.applyRight('./.tmp', './dist', ch)).finally(()=>process.exit(0));\" && rimraf ./.tmp",
"watch": "watch \"npm run build\" ./src --interval 1 --wait 0.1"
},
"author": "",
"license": "ISC",
"devDpendencies": {
"diff-file-tree": "^2.3.2",
"rimraf": "^3.0.0",
"typescript": "^3.6.4",
"watch": "^1.0.2"
}
}
For cleaning up of not actual compilation results I use this https://github.com/whitecolor/ts-clean-built
Any update on this one? It's still causing a headache...
Still causing problems...
I would think this feature could use the build info from —incremental to know what needs to be deleted.
Any updates on this?
Also facing this problem, would be a very nice addition
Bump
Just wanted to add that none of the 3rd party library workarounds mentioned above take care of cleaning up the .tsbuildinfo file. It keeps info about the deleted source file. Probably not a huge deal, but could lead to bloat with large refactors.
For
tsc --watchwe have a list of output files from the previous iteration, we just need to diff that with the current list, and delete the old outputs.
I found this issue after a bit of searching since this is still a very common source of bugs and poses an unfortunate workflow problem with tsc --watch. In fact, my initial conclusion is that I should be rolling my own watch that invokes tsc -p on individual files that are changed.
My question here is, why would our approach be to maintain a list of files necessarily? The underlying FS watch should receive events on file deletion or move (typically a "rename" event). On rename, we simply check if a corresponding JS file exists in the outDir that matches the file that had moved or been deleted, and delete that JS file. This is a stateless operation and hopefully shouldn't be too hard to implement, but what am I missing? (this is the approach I am going to take when doing this in user space anyways).
This a daunting issue, especially if you refactor often, which you should!
And this, is the story about my pursuit to fixing it, the 2023 way! Of course with the help of AI, ChatGTP in particular. So keep reading...
Initially, I googled the issue with "typescript tsc watch remove deleted files", which is how we used to trouble shoot always. Google responded with current issue first, which was frustratingly still open after 6 years (!).
I followed more links but none seemed like solving the problem inherently:
Reading the current issue, I found ts-clean-built to be the best solution & project around at this time. Before even giving it a spin, with only issue, it seemed it would save me hours of wasted effort ;-)
But don't we all have ChatGPT plugins right now, next to the google search? It's just a click away, or as a paid customer you get ALWAYS ChatGPT's answer. Which read as:
ChatGPT
When using tsc with the --watch flag, it will recompile the files every time there is a change in the project directory. However, by default, tsc doesn't remove files that have been deleted from the project directory. To remove the deleted files from the output directory, you can use the --removeComments flag.
Here's an example command that uses both --watch and --removeComments:
tsc --watch --removeComments
The --removeComments flag removes all comments from the output files and also removes any files that no longer exist in the source directory.
If you want to keep comments in the output files, you can use the --clean flag instead, which removes any files that no longer exist in the source directory without removing comments:
tsc --watch --clean
Note that if you are using a version of tsc older than 3.8, the --removeComments flag will not work as intended. Instead, you can use the --preserveWatchOutput flag, which will also remove deleted files:
tsc --watch --preserveWatchOutput
Keep in mind that this flag is only available in TypeScript version 3.8 and newer.
Wow! Is that true? Did Chat GPT overwhelmingly solved my ;issue with one --clean commmand, but googled failed to tell me?
So I immediately added the --clean flag to my scripts: npx tsc --watch --clean -p tsconfig.json. Of course it failed:
error TS5093: Compiler option '--clean' may only be used with '--build'.
ChatGPT let down No 1. For a start, --clean can be used only along side --build for some reason something ChatGPT could and maybe should have known! Not sure about the dates tho.
So, lets try again:
npx tsc --clean --watch --build -p tsconfig.json
And this time I get:
ERROR: "error TS6369: Option '--build' must be the first command line argument.
Why so stringent? Another try...
npx tsc --build --clean --watch -p tsconfig.json
Guess what! Two errors this time!
error TS5072: Unknown build option '-p'.
error TS6370: Options 'clean' and 'watch' cannot be combined.
Ok, I had enough! In any case, --build --clean are useless when watching, its doesnt "mirror" src/ to dist/ amd has a myriad of issues - see https://stackoverflow.com/questions/68144771/whats-the-difference-between-tsc-build-clean-vs-rm-rf-js
So I tried on last thing, although I was sure its doomed. According to ChatGPT The --removeComments flag removes all comments from the output files and also removes any files that no longer exist in the source directory.. Lets see:
As one would expect, it doesn't, it is just to Disable emitting comments..
So ChatGPT lied and tried to fool me twice in one problem, as many as the solutions! Welcome to the Brave New World of lies, AI powered!
TypeScript team, please, please solve this issue! Till then I'm going for ts-clean-built!
To anyone looking for a solution, I've completely stopped using tsc --watch and instead relied on chokidar-cli for watching over my files and triggering the tsc build command.
I don't know if it is as efficient as tsc --watch command but at least it works.
{
"name": "myproject",
"scripts": {
"watch": "chokidar \"src/*\" \"tsconfig.json\" -c \"npm run build\" --initial",
"build": "rimraf dist && tsc"
},
"devDependencies": {
"@types/node": "^20.11.17",
"chokidar-cli": "^3.0.0",
"rimraf": "^5.0.5",
"typescript": "^5.3.3"
}
}
For anyone have this problem like me, i have made a library to solve this, and many more, take a look. typemon
I came up with this solution:
- Add
onchange
yarn add -D onchange
- Configure
package.json
{
"scripts": {
"clean": "./scripts/delete_old_files.cjs --dist dist --src src",
"watch": "concurrently \"tsc -w\" \"onchange -f unlink 'src/**/*' -- yarn clean\"",
"build": "yarn clean && tsc"
}
}
- Add
./scripts/delete_old_files.cjs
#!/usr/bin/env node
/**
* Deletes files from dist/ that do not exist in src/
*/
const fs = require('fs')
const path = require('path')
const { program } = require('commander')
program
.requiredOption('--src <source dir>', 'Source directory')
.requiredOption('--dist <destination dir>', 'Destination directory')
.option('--verbose', 'Print deleted file names')
.option('--silent', 'Print nothing')
program.parse(process.argv)
const options = program.opts()
const srcDir = path.resolve(options.src)
const distDir = path.resolve(options.dist)
let removedFilesCount = 0
const getCorrespondingSrcFile = (distFile) => {
const relPath = path.relative(distDir, distFile)
const srcFileTs = path.join(srcDir, relPath.replace(/\.js$/, '.ts'))
const srcFileJson = path.join(srcDir, relPath)
if (fs.existsSync(srcFileTs)) return srcFileTs
if (fs.existsSync(srcFileJson)) return srcFileJson
return null
}
const cleanDist = (distPath) => {
if (!fs.existsSync(distPath)) {
console.error(`Dist directory does not exist: ${distPath}`)
process.exit(1)
}
const filesToCheck = fs.readdirSync(distPath, { withFileTypes: true })
for (const file of filesToCheck) {
const distFilePath = path.join(distPath, file.name)
if (file.isDirectory()) {
cleanDist(distFilePath)
if (fs.readdirSync(distFilePath).length === 0) {
fs.rmdirSync(distFilePath)
}
continue
}
const correspondingSrcFile = getCorrespondingSrcFile(distFilePath)
if (!correspondingSrcFile) {
removedFilesCount++
if (!options.silent) {
if (options.verbose) {
console.log(`Removing: ${distFilePath}`)
}
}
fs.unlinkSync(distFilePath)
}
}
}
cleanDist(distDir)
if (!options.silent) {
console.log(`Total old files removed: ${removedFilesCount}`)
}
- And of course
chmod +x ./scripts/delete_old_files.cjs
It is now 2025 😬
I realize that the advantage of tsc -- watch is that it is not forced to compile everything every time you change a few files, but this feature would be sooo nice if it is not too much of a nightmare to implement.