-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #176 from pmcelhaney/170-transform-typescript-file…
…s-on-the-fly add a transpiler class that watches TS file and transforms them
- Loading branch information
Showing
3 changed files
with
194 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); | ||
}); |