Skip to content

Commit

Permalink
Merge pull request #176 from pmcelhaney/170-transform-typescript-file…
Browse files Browse the repository at this point in the history
…s-on-the-fly

add a transpiler class that watches TS file and transforms them
  • Loading branch information
pmcelhaney committed Sep 7, 2022
2 parents fa4343b + 40e05fd commit 612ebf9
Show file tree
Hide file tree
Showing 3 changed files with 194 additions and 1 deletion.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"eslint-config-hardcore": "24.15.0",
"eslint-formatter-github-annotations": "0.1.0",
"eslint-import-resolver-typescript": "^3.2.5",
"eslint-plugin-etc": "^2.0.2",
"eslint-plugin-file-progress": "^1.3.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jest": "^27.0.1",
Expand All @@ -65,6 +66,7 @@
"jsonwebtoken": "^8.5.1",
"koa": "^2.13.4",
"koa-bodyparser": "^4.3.0",
"prettier": "^2.7.1"
"prettier": "^2.7.1",
"typescript": "^4.8.2"
}
}
77 changes: 77 additions & 0 deletions src/transpiler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import fs from "node:fs/promises";
import nodePath from "node:path";
import { constants as fsConstants } from "node:fs";
import { once } from "node:events";

import ts from "typescript";
import chokidar from "chokidar";

async function ensureDirectoryExists(filePath) {
const directory = nodePath.dirname(filePath);

try {
await fs.access(directory, fsConstants.W_OK);
} catch {
await fs.mkdir(directory, { recursive: true });
}
}

export class Transpiler extends EventTarget {
constructor(sourcePath, destinationPath) {
super();
this.sourcePath = sourcePath;
this.destinationPath = destinationPath;
}

async watch() {
this.watcher = chokidar.watch(`${this.sourcePath}/**/*.{js,mjs,ts,mts}`);

this.watcher.on("all", async (eventName, sourcePath) => {
const destinationPath = sourcePath
.replace(this.sourcePath, this.destinationPath)
.replace(".ts", ".js");

if (["add", "change"].includes(eventName)) {
this.transpileFile(eventName, sourcePath, destinationPath);
}

if (eventName === "unlink") {
try {
await fs.rm(destinationPath);
} catch (error) {
if (error.code !== "ENOENT") {
throw error;
}
}

this.dispatchEvent(new Event("delete"));
}
});
await once(this.watcher, "ready");
}

async stopWatching() {
await this.watcher?.close();
}

async transpileFile(eventName, sourcePath, destinationPath) {
await ensureDirectoryExists(destinationPath);

const source = await fs.readFile(sourcePath, "utf8");

const result = ts.transpileModule(source, {
compilerOptions: { module: ts.ModuleKind.ES2022 },
}).outputText;

await fs.writeFile(
nodePath.join(
sourcePath
.replace(this.sourcePath, this.destinationPath)
.replace(".ts", ".js")
),
result
);

this.dispatchEvent(new Event("write"));
}
}
114 changes: 114 additions & 0 deletions test/transpiler.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { once } from "node:events";
import fs from "node:fs/promises";
import { constants as fsConstants } from "node:fs";

import { Transpiler } from "../src/transpiler.js";

import { withTemporaryFiles } from "./lib/with-temporary-files.js";

const TYPESCRIPT_SOURCE = "const x:number = 1;\n";
const JAVASCRIPT_SOURCE = "var x = 1;\n";

describe("a Transpiler", () => {
// eslint-disable-next-line init-declarations
let transpiler;

// eslint-disable-next-line jest/no-hooks
afterEach(() => {
transpiler.stopWatching();
});

it("finds a file and transpiles it", async () => {
const files = {
"src/found.ts": TYPESCRIPT_SOURCE,
};

await withTemporaryFiles(files, async (basePath, { path }) => {
transpiler = new Transpiler(path("src"), path("dist"));

await transpiler.watch();

await once(transpiler, "write");

await expect(fs.readFile(path("dist/found.js"), "utf8")).resolves.toBe(
JAVASCRIPT_SOURCE
);

transpiler.stopWatching();
});
});

it("discovers a new file and transpiles it", async () => {
// on Linux the watcher doesn't seem to work consistently if there's not a file in the directory to begin with

await withTemporaryFiles(
{ "src/starter.ts": TYPESCRIPT_SOURCE },
async (basePath, { path, add }) => {
transpiler = new Transpiler(path("src"), path("dist"));

const writeTheFirstFile = once(transpiler, "write");

await transpiler.watch();
await writeTheFirstFile;

const write = once(transpiler, "write");

await add("src/added.ts", TYPESCRIPT_SOURCE);
await write;

await expect(fs.readFile(path("dist/added.js"), "utf8")).resolves.toBe(
JAVASCRIPT_SOURCE
);

transpiler.stopWatching();
}
);
});

it("sees an updated file and transpiles it", async () => {
const files = {
"src/update-me.ts": "const x = 'code to be overwritten';\n",
};

await withTemporaryFiles(files, async (basePath, { path, add }) => {
transpiler = new Transpiler(path("src"), path("dist"));

const initialWrite = once(transpiler, "write");

await transpiler.watch();
await initialWrite;

const overwrite = once(transpiler, "write");

add("src/update-me.ts", TYPESCRIPT_SOURCE);
await overwrite;

await expect(
fs.readFile(path("dist/update-me.js"), "utf8")
).resolves.toBe(JAVASCRIPT_SOURCE);

transpiler.stopWatching();
});
}, 10_000);

it("sees a removed TypeScript file and deletes the JavaScript file", async () => {
const files = {
"src/delete-me.ts": TYPESCRIPT_SOURCE,
};

await withTemporaryFiles(files, async (basePath, { path, remove }) => {
transpiler = new Transpiler(path("src"), path("dist"));

await transpiler.watch();
await once(transpiler, "write");
await remove("src/delete-me.ts");
await once(transpiler, "delete");

await expect(() =>
fs.access(path("dist/delete-me.js"), fsConstants.F_OK)
).rejects.toThrow(/ENOENT/u);

transpiler.stopWatching();
});
});
});

0 comments on commit 612ebf9

Please sign in to comment.