tscw-config v1.0.0
Motivation
Running tsc locally will compile the closest project defined by a
tsconfig.json
, or you can compile a set of TypeScript files by passing in a glob of files you want. When input files are specified on the command line, tsconfig.json files are ignored. - tsc CLI Options
tscw-config
lets you run tsc
on files while keeping tsconfig.json
respected.
!NOTE >
tscw-config
stands for tsc with config.
Use cases
A common use case for running tsc
on certain files is when used in a pre-commit hook. e.g. lint-staged.
For example, you may want to type-check staged files by running tsc --noEmit foo.ts bar.ts
. In this case tsc
will ignore the tsconfig.json
, using -p tsconfig.json
with files will result in an error.
You can explicitly pass the CLI options in. e.g. --strict --allowSyntheticDefaultImports ...
to tsc
, but that can be tedious.
Using tscw
is much easier: tscw --noEmit foo.ts bar.ts -p tsconfig.json
.
Getting Started
tscw
seamlessly integrates with most popular package managers, including:
- npm
- pnpm
- Yarn
- Yarn (Plug’n’Play)
npm:
npm i -D tscw-config
pnpm:
pnpm add -D tscw-config
yarn:
yarn add -D tscw-config
Usage
After installing tscw-config
, you can use tscw
the same way you use tsc
, but tscw
will not ignore your tsconfig.json
when files are specified.
By default, tscw
uses the root tsconfig.json
if no one is specified.
# root tsconfig.json is used
npx tscw foo.ts
# specify a tsconfig
npx tscw --noEmit foo.ts -p ./config/tsconfig.json
# or
npx tscw --noEmit foo.ts --project ./config/tsconfig.json
# match ./foo.ts, ./bar.ts ...
npx tscw *.ts
# match ./foo/baz.ts, ./bar/foo.ts ...
npx tscw **/*.ts
# you can even use it without any files specified
npx tscw --noEmit # it is the same as npx tsc --noEmit
Here's an example of using it in a .lintstagedrc.js
file. You can also check out the .lintstagedrc.mjs in this project.
/**
* Passing absolute path is fine, but relative path is cleaner in console.
* @param {string[]} files
*/
const typeCheck = files => {
const cwd = process.cwd();
const relativePaths = files.map(file => path.relative(cwd, file)).join(" ");
return `npx tscw --noEmit ${relativePaths}`;
};
export default {
"**/*.{ts,mts,cts,tsx}": [prettier, typeCheck, eslint],
};
if your're using yarn PnP, instead of using npx tscw
, use yarn tscw
:
yarn tscw foo.ts
!NOTE
tscw
supports all CLI options supported bytsc
.
API
tscw-config
also exposes a function to run tsc
programmatically, but in most cases you should use the CLI tscw
:
import tscw from 'tscw-config';
const result = await tscw`foo.ts --noEmit -p tsconfig.json`
// or
const result = await tscw("foo.ts", "--noEmit", "-p", "tsconfig.json");
Return type
type Result = Promise<SpawnResult | SpawnError>;
interface SpawnResult {
pid: number;
exitCode: number;
stdout: string;
stderr: string;
}
interface SpawnError {
pid: null;
exitCode: number;
stderr: string;
stdout: null;
}
In the following scenarios, the function returns Promise<SpawnError>
:
- No
package.json
is found in the root of your project. - No
tsconfig.json
is found in the root of your project if no tsconfig is passed to the function. - Specified files not found.
- Missing argument for
-p
or--project
.
import tscw from "tscw-config";
const result = await tscw`foo.ts --noEmit -p noSuchFile.json`;
/*
result: {
pid: null,
exitCode: 1,
stderr: "Can't find noSuchFile.json",
stdout: null,
};
*/
Otherwise the function returns Promise<SpawnResult>
, which means that the args are successfully passed to tsc
.
Under the hood, tscw
uses spawn
to run tsc
, the result from tsc
is stored in result.stdout
even when exitCode is not 0
.
// containTypeError.ts
type A = number;
const _a: A = "";
import tscw from "tscw-config";
const result1 = await tscw`containTypeError.ts --noEmit -p tsconfig.json --pretty false`;
console.log(result1.pid); // number
console.log(result1.exitCode); // 1
console.log(result1.stdout); // "containTypeError.ts(3,7): error TS2322: Type 'string' is not assignable to type 'number'.\r\n"
console.log(result1.stderr); // ""
const result2 = await tscw`noTypeError.ts --noEmit -p tsconfig.json`;
console.log(result2.pid); // number
console.log(result2.exitCode); // 0
console.log(result2.stdout); // ""
console.log(result2.stderr); // ""
!NOTE
By default,stdout
contains ANSI escape code, if you wantstdout
to be plain text, pass--pretty false
to the function./* "\x1B[96mcontainTypeError.ts\x1B[0m:\x1B[93m3\x1B[0m:\x1B[93m7\x1B[0m - \x1B[91merror\x1B[0m\x1B[90m TS2322: \x1B[0mType 'string' is not assignable to type 'number'.\r\n" + '\r\n' + '\x1B[7m3\x1B[0m const _a: A = "";\r\n' + '\x1B[7m \x1B[0m \x1B[91m ~~\x1B[0m\r\n' + '\r\n' + '\r\n' + 'Found 1 error in containTypeError.ts\x1B[90m:3\x1B[0m\r\n' + '\r\n' */
Notice that when you pass a file to the function using a relative path, it is relative to the current working directory (cwd) when you run the script, not relative to the file where this function is used.
!IMPORTANT
In most cases, you should use the CLItscw
when you want the process to fail if the compilation fails. For example in CI pipeline, lint-staged, etc. Executing the function will not cause the process to fail even if the returnedexitCode
is not0
, unless you explicitly exit the process with the returnedexitCode
, liketscw
does.
How it works
- Argument Parsing:
- The script processes user-specified arguments to handle flags and file paths.
- Finding
tsconfig.json
:- If no
tsconfig.json
file is specified via the-p
or--project
flag, the nearesttsconfig.json
file will be used for the current workspace. - The script first looks for the current working directory, if not found, it goes all the way up until the level where
package.json
is located.
- If no
- Temporary File:
- A temporary file is created to store the content of the
tsconfig.json
file being used. - It adds/replaces the
"files"
field with the files specified. - It empties the
"include"
field.
- A temporary file is created to store the content of the
- Running
tsc
:- It runs
tsc
with the temp file and any specified flags.
- It runs
- Cleanup:
- The script removes the temporary file when the script exits or receives certain signals(SIGINT, SIGHUP, SIGTERM).
!NOTE
Windows has limited support for process signals compared to Unix-like systems, especially whenprocess.kill
is used to terminate a process, signal will not be caught by the process, therefore cleaning up the temp file is a problem. See Signal events.Technically, to fix the cleanup problem, using
options.detached
for a child process would be enough, but lint-staged takes the approach of terminating all the child processes by callingprocess.kill
on the tasks that areKILLED
(When multiple tasks are running concurrently, if one taskFAILED
, other tasks will beKILLED
).In order to properly fix this problem,
tscw-config
creates a daemon to handle the cleanup task if it is running on Windows. The daemon will exit gracefully after the temporary file is deleted or, at most, after 1 minute.
10 months ago