README
Chokidar Ts
Typescript compiler using chokidar vs native Fs events.
This module uses the compiler API of typescript to work as replacement for tsc
and tsc --watch
and uses chokidar for watching file changes.
Table of contents
- Why not simply use tsc?
- Why this module?
- Goals
- How it works?
- Customer Transformers
- Installation
- Usage
- API Docs
- Debug
- Reference Tree
❓ Upgrading from 2.x.x
-
The emitted events now emits an object with
relativePath
andabsPath
propertieswatcher.on('add', ({ relativePath, absPath }) => { })
-
Now you have to define an explicit watch mode when creating the
watcher
instance.const lspWatcher = compiler.watcher(config!, 'lsp') const watcher = compiler.watcher(config!, 'raw')
Why not simply use tsc?
You must use tsc
, since it is the official command line tool provided by the Typescript team. However, it has following restrictions.
- It only watches for typescript source files.
- Uses
fs.watch
orfs.watchFile
. Both have their own issues and that's why modules like chokidar were created. - There is no way to hook custom transformers when using
tsc
.
If all of the above problems doesn't impact your projects, then simply use
tsc
and do waste time looking for alternatives.
Because of the above restrictions (and many more), communities like webpack and gulp also has to use the compiler API to add support for typescript in their build tools.
Why this module?
If you are user of Webpack or gulp and working in frontend space, then your life is all set, since they have first class support for Typescript projects.
However, I maintain a Node.js framework, which is bit different from frontend projects and has some unique challenges.
A backend project may have other files from .ts
files. For example: Restart the server when .env
file changes or when there is a change in a view template. Because of this, you will see many backend projects using the nodemon
watcher to watch for all files and then rebuild the typescript project on every change and they end up making the build process too slow.
I created this module to address the above defined workflow by using the Language Service API of Typescript and build only the changed files.
Goals
The goal of this module is to stay as close as possible to the behavior of tsc
and tsc --watch
, while addressing the above mentioned issues.
- Always rely on
tsconfig.json
file and do not invent new configuration options. - Use much of the defaults from the compiler API. We are not set out to create a compiler with different approach all together.
- Allow custom AST transformers
How it works?
I make sure not to over engineer the process of compiling the code and keep it identical to the workings of tsc
.
The module exposes 3 main sub-modules.
ConfigParser
The ConfigParser
module exposes the API to parse the typescript config
Builder
The Builder
module exposes the API to build the entire project. It is similar to tsc
.
LSP Watcher
This is where things get's interesting. Instead of using the native fs
events (which are super slow), we make use of chokidar
to watch the entire project and handle file changes, as explained below.
Is Typescript file?
Handle the event internally and process the file using the Language service API. We also check the file path against the includes
and excludes
to make sure, that we are processing the right files.
If not a typescript file?
We will emit add
, change
or unlink
event, so that you (the module consumer) can use and decide what to do on that event. For example: If filePath is .env
, then restart the Node.js server.
By using this flow, you will always have one watcher in your entire project, that will process the Typescript files, restart the Node.js server or copy files to build folder.
Watcher
The Watch is similar to the LSP watcher, but instead of compiling files using the Typescript compiler, it will just emit the events.
This is helpful when you are running your application using a module like ts-node or @adonisjs/require-ts but want the watcher to restart the HTTP server on file change.
Instead of using a standard file watcher. The watcher class uses Typescript config to decide which files to watch or ignore.
Customer Transformers
You can also define custom transformers to transform the AST. You can read more about the transform API by following this article series.
Installation
Install the module from npm registry as follows:
npm i @poppinss/chokidar-ts
# yarn
yarn add @poppinss/chokidar-ts
Usage
import { TypescriptCompiler } from '@poppinss/chokidar-ts'
const compiler = new TypescriptCompiler(
__dirname,
'tsconfig.json',
require('typescript/lib/typescript')
)
The constructor accepts three arguments:
project root
: The path to the project root.config file
: The name of the config file from where to read the configuration.typescript
: You must pass in the typescript reference, that is used by your project.
configParser(compileOptionsToExtend?: ts.CompilerOptions)
Parse the project config. Optionally, you can define your custom compiler options. There are helpful, when you want to overwrite some of the values from the tsconfig.json
file.
const { error, config } = compiler.configParser().parse()
/**
* Unable to read the config at all
*/
if (error) {
console.log(error)
return
}
/**
* Config has been processed, but has some errors
*/
if (config && config.errors.length) {
console.log(config.errors)
return
}
// Use config
builder(options: ts.ParsedCommandLine)
Build the project. It is same as running tsc
command. However, the incremental: true
will have no impact.
The build
command is used to build the project from scratch, it indirectly means, we should cleanup the old build before running this command and hence incremental: true
has no impact once old build is deleted.
Why Delete the Old Build?
Because, the typescript compiler is not smart enough to delete the compiled file once the source file has been deleted and you will end up having files inside your build directory which doesn't even exists inside the source.
Deleting the build and re-building the project results in the most consistent and reliable output.
const { error, config } = compiler.configParser().parse()
if (error || !config) {
console.log(error)
return
}
if (config.errors.length) {
console.log(config.errors)
return
}
const { diagnostics, skipped } = compiler.builder(config!).build()
if (diagnostics.length) {
console.log('Built with few errors')
console.log(diagnostics)
} else {
console.log('Built successfully')
}
watcher(options: ts.ParsedCommandLine, mode: 'raw' | 'lsp')
Returns an instance of watcher that uses chokidar
and Typescript LanguageService
to compile the files as they change.
const { error, config } = compiler.configParser().parse()
if (error || !config) {
console.log(error)
return
}
if (config.errors.length) {
console.log(config.errors)
return
}
const watcher = compiler.watcher(config!, 'lsp')
watcher.on('watcher:ready', () => {
// Watcher is ready
})
watcher.on('subsequent:build', ({ relativePath, absPath, skipped, diagnostics }) => {
// re-built source files
})
watcher.on('add', ({ relativePath, absPath }) => {
// file other than `.ts` files has been added
})
watcher.on('change', ({ relativePath, absPath }) => {
// file other than `.ts` files has changed
})
watcher.on('unlink', ({ relativePath, absPath }) => {
// file other than `.ts` files has been removed
})
watcher.on('source:unlink', ({ relativePath, absPath }) => {
// source file removed
})
watcher.watch(['.'], {
ignored: ['node_modules', 'build'],
})
// Stop the watcher anytime you want to
watcher.chokidar.close()
When you choose raw
mode over the lsp
mode, then instead of emitting subsequent:build
, it will emit following events.
source:add
source:change
source:unlink
use(transformer: PluginFn, lifecycle: 'before' | 'after')
Define your custom transformer. The transformer will receive the parsed config and the typescript
reference passed to the constructor.
compiler.use((ts, config) => {
return function transformer(ctx) {}
}, 'after')
API Docs
Following are the autogenerated files via Typedoc
Debug
You can debug the behavior of this module by running it as DEBUG=tsc:* node script-file
Reference Tree
In watch mode, we need to maintain a reference tree of files to re-process dependent files when a given file changes. This is how it works:
Let's say your project has just two file.
.
└── foo.ts
└── bar.ts
The foo.ts
file has a dependency on bar.ts
file.
// foo.ts
import { greet } from './bar'
console.log(greet('virk'))
// bar.ts
export function greet(name: string) {
return `Hello ${name}`
}
When the bar.ts
file changes, we also have to re-process the foo.ts
to ensure that it is still valid.
To achieve the defined behavior, we maintain a reference tree of all the source files mentioned inside includes
and not inside excludes
of the tsconfig.json
file.
Reference tree for node_modules
is not maintained. So, if you update a package, you will have to re-start the compiler.