Skip to content

Commit

Permalink
feat: init
Browse files Browse the repository at this point in the history
  • Loading branch information
zanminkian committed Apr 14, 2024
1 parent fffe2e1 commit ca8c9b0
Show file tree
Hide file tree
Showing 10 changed files with 486 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/tough-tigers-notice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rnm/tscx": patch
---

feat: init
71 changes: 69 additions & 2 deletions packages/tscx/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,70 @@
# tscx(WIP)
# TSCX

A tsc wrapper with many convenient features.
[![](https://img.shields.io/npm/l/@rnm/tscx.svg)](https://github.com/rnmjs/tscx/blob/main/LICENSE)
[![](https://img.shields.io/npm/v/@rnm/tscx.svg)](https://www.npmjs.com/package/@rnm/tscx)
[![](https://img.shields.io/npm/dm/@rnm/tscx.svg)](https://www.npmjs.com/package/@rnm/tscx)
[![](https://img.shields.io/librariesio/release/npm/@rnm/tscx)](https://www.npmjs.com/package/@rnm/tscx)
[![](https://packagephobia.com/badge?p=@rnm/tscx)](https://packagephobia.com/result?p=@rnm/tscx)

A `tsc` wrapper with many convenient features. Bring the [nodemon](https://www.npmjs.com/package/nodemon) + JavaScript development experience to TypeScript.

## Background

When we are using JavaScript, we usually run `nodemon main.js`. Then, application will automatically restart when js file changes. It's a great development experience. Why can't TypeScript? The reason is the compilation (tsc). Because of this, some edge cases are inconvenient just using `tsc`. For example:

- Remove the output folder before compilation started
- Copy non-ts files to output folder after compilation finished
- Execute app entrance file immediately once compilation finished
- Watch source files, repeat steps above and restart the app

Now you can run one line of command to solve the problems. Better development experience!

```sh
npx tscx --project tsconfig.build.json --remove --copyfiles --watch --exec bootstrap.js
```

Happy hacking!

## Highlight

- Same usages as `tsc` with few additional options.
- Remove output folder before every compilation.
- Copy non-ts files to output folder after every compilation.
- Execute js file after compilation success.
- Respect `tsconfig.json`.
- ESM.

## Install

```sh
npm install typescript @nrm/tscx -D
```

## Usages

```sh
# Equivalent to `npx tsc`
$ npx tscx

# Equivalent to `npx tsc --project tsconfig.build.json --watch`
$ npx tscx --project tsconfig.build.json --watch

# Remove output folder before compilation and then compile ts code.
$ npx tscx --remove

# Compile ts code and then copy non-ts files to output folder after compilation.
$ npx tscx --copyfiles

# Compile ts code and execute bootstrap.js after successful compilation.
$ npx tscx --exec bootstrap.js

# Compile ts code in watch mode and execute bootstrap.js after every successful compilation.
$ npx tscx --project tsconfig.build.json --watch --exec bootstrap.js

# Remove => Compile => Copy => Execute => Edit any file to repeat it
$ npx tscx --project tsconfig.build.json --remove --copyfiles --watch --exec bootstrap.js
```

## License

MIT
9 changes: 8 additions & 1 deletion packages/tscx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"rm",
"watch",
"tsc-watch",
"dev"
"node-dev",
"restart"
],
"repository": {
"type": "git",
Expand All @@ -20,6 +21,12 @@
"license": "MIT",
"author": "hellozmj@qq.com",
"type": "module",
"bin": {
"tscx": "./dist/bin/tscx.js"
},
"scripts": {
"build": "tsc -p tsconfig.build.json"
},
"dependencies": {
"chokidar": "3.6.0",
"commander": "12.0.0"
Expand Down
75 changes: 75 additions & 0 deletions packages/tscx/src/action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import path from "node:path";
import process from "node:process";
import chokidar, { type FSWatcher } from "chokidar";
import { Compiler, type CompilerOptions } from "./compiler.js";

interface TscxOptions extends CompilerOptions {
watch: boolean;
}

export class Action {
private readonly compiler;
private watcher?: FSWatcher;
constructor(private readonly options: TscxOptions) {
this.compiler = new Compiler(options);
}

private setupWatcher() {
const include = this.compiler.getInclude() ?? [];
const watchFiles =
include.length <= 0
? [process.cwd()]
: include
.map((i) => path.resolve(process.cwd(), i))
.concat(path.resolve(process.cwd(), this.options.project));

this.watcher = chokidar.watch(watchFiles, {
ignored: ["**/node_modules/**", "**/.git/**", this.compiler.getOutDir()],
ignoreInitial: true,
});
this.watcher
.on("add", (filepath) => this.cb(filepath))
.on("unlink", (filepath) => this.cb(filepath))
.on("change", (filepath) => this.cb(filepath))
.on("ready", () => this.cb());
}

private cb(filepath?: string) {
console.log("Recompile for the file updated", filepath);
if (
!filepath ||
path.resolve(process.cwd(), filepath) !==
path.resolve(process.cwd(), this.options.project)
) {
return this.compiler.exec();
}

try {
this.compiler.refreshTsConfig();
} catch (e) {
console.warn(
"Refresh ts config fail. You can ignore this small warning.",
e,
);
return;
}
this.watcher
?.close()
.then(() => {
this.setupWatcher();
})
.catch((e) => {
console.error("Close watcher fail!", e);
process.exit(1);
});
}

start() {
if (!this.options.watch) {
this.compiler.exec();
return;
}

this.setupWatcher();
}
}
46 changes: 46 additions & 0 deletions packages/tscx/src/bin/tscx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { Command } from "commander";
import { Action } from "../action.js";

const version: string = JSON.parse(
await fs.readFile(
path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"..",
"..",
"package.json",
),
"utf8",
),
).version;

new Command()
.name("tscx")
.version(version)
.description("The TypeScript Compiler. Run `tsc` under the hood.")
.option(
"-p, --project <path>",
"Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.",
"tsconfig.json",
)
.option("-w, --watch", "Watch input files.", false)
.option(
"-r, --remove",
"Remove output folder before before every compilation.",
false,
)
.option(
"-c, --copyfiles",
"Copy non-ts files to output folder after every compilation.",
false,
)
.option(
"-e, --exec <path>",
"Execute the specified js file after compilation success",
)
.action((options) => {
new Action(options).start();
})
.parse();
46 changes: 46 additions & 0 deletions packages/tscx/src/cmd/copyfiles.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import fs from "node:fs/promises";
import path from "node:path";
import process from "node:process";

/**
* Copy non-ts/non-js files to outDir
* @param rootDir absolute path
* @param outDir absolute path
*/
async function copyfiles(rootDir: string, outDir: string) {
rootDir = path.resolve(rootDir);
outDir = path.resolve(outDir);
async function walkDir(dir: string, cb: (filepath: string) => Promise<void>) {
await Promise.all(
(await fs.readdir(dir))
.map((filepath) => path.resolve(dir, filepath))
.map(async (filepath) => {
if ((await fs.stat(filepath)).isDirectory()) {
if (
!filepath.startsWith(outDir) &&
!filepath.endsWith(`${path.sep}node_modules`)
) {
await walkDir(filepath, cb);
}
} else {
if (!/\.(js|cjs|mjs|jsx|ts|cts|mts|tsx)$/.test(filepath)) {
await cb(filepath);
}
}
}),
);
}
await walkDir(rootDir, async (filepath) => {
const dest = filepath.replace(rootDir, outDir);
console.log("Copy", filepath, "=>", dest);
await fs.copyFile(filepath, dest);
});
}

const rootDir = process.argv[2];
const outDir = process.argv[3];
if (!rootDir || !outDir) {
throw new Error("`rootDir` and `outDir` are required");
}

await copyfiles(rootDir, outDir);
37 changes: 37 additions & 0 deletions packages/tscx/src/cmd/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { spawn } from "node:child_process";
import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REMOVE_PATH = path.resolve(__dirname, "remove.mjs");
const COPYFILES_PATH = path.resolve(__dirname, "copyfiles.mjs");
const TSC_PATH = path.resolve(
process.cwd(),
"node_modules",
"typescript",
"bin",
"tsc",
);

export function remove(filepath: string) {
console.log("Remove", filepath);
return spawn("node", [REMOVE_PATH, filepath], { stdio: "inherit" });
}

export function tsc(options: { project: string }) {
console.log("Tsc", options);
return spawn("node", [TSC_PATH, "--project", options.project], {
stdio: "inherit",
});
}

export function copyfiles(rootDir: string, outDir: string) {
console.log("Copyfiles", rootDir, "=>", outDir);
return spawn("node", [COPYFILES_PATH, rootDir, outDir], { stdio: "inherit" });
}

export function exec(filepath: string) {
console.log("Execute", filepath);
return spawn("node", [filepath], { stdio: "inherit" });
}
24 changes: 24 additions & 0 deletions packages/tscx/src/cmd/remove.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import fs from "node:fs";
import process from "node:process";

/**
* @param filepath absolute filepath
*/
async function remove(filepath: string) {
await new Promise<void>((resolve, reject) => {
fs.stat(filepath, (err) => {
if (err) {
return err.code === "ENOENT" ? resolve() : reject(err); // do nothing if file not found
}
fs.rm(filepath, { recursive: true }, (e) => (e ? reject(e) : resolve()));
});
});
console.log(`Removed ${filepath}`);
}

const filepath = process.argv[2];
if (!filepath) {
throw new Error("File path is required");
}

await remove(filepath);

0 comments on commit ca8c9b0

Please sign in to comment.