diff --git a/justfile b/justfile index a4282ee..ff58d36 100644 --- a/justfile +++ b/justfile @@ -9,7 +9,7 @@ coverage: bun-test deno-coverage check: deno-check bun-check docs: - deno doc --html --unstable-ffi --name="Scroll" ./src/scroll.ts ./src/common/mod.ts ./src/deno/mod.ts ./src/bun/mod.ts + deno doc --html --unstable-ffi --name="Scroll" ./src/scroll.ts ./src/common/*.ts ./src/deno/mod.ts ./src/bun/mod.ts # Reformat the code fmt: @@ -23,6 +23,7 @@ quality: check test # Clean up any generated files clean: + rm -f test.file rm -rf .deno-cover rm -rf coverage rm -rf docs @@ -66,3 +67,16 @@ deno-coverage: # Run with deno deno-run file="": deno run --allow-all --allow-ffi --deny-hrtime --unstable-ffi ./src/scroll.ts {{file}} + +########################################################################################## +# tsx(Node JS)-specific commands +########################################################################################## + +# Test with tsx (NodeJS) +tsx-test: + npx tsx --test './src/common/all_test.ts' + +# Run with tsx (NodeJS) +tsx-run file="": + npx tsx ./src/scroll.ts {{file}} + diff --git a/package.json b/package.json index 8ac0b9c..ad5b2ad 100644 --- a/package.json +++ b/package.json @@ -2,5 +2,8 @@ "dependencies": {}, "devDependencies": { "bun-types": "^1.0.11" - } + }, + "scripts": { + }, + "type": "module" } diff --git a/src/bun/mod.ts b/src/bun/mod.ts index 743d89d..64ea653 100644 --- a/src/bun/mod.ts +++ b/src/bun/mod.ts @@ -6,8 +6,11 @@ import { IRuntime, RunTimeType } from '../common/runtime.ts'; import BunTerminalIO from './terminal_io.ts'; import BunFileIO from './file_io.ts'; -import * as process from 'node:process'; +import process from 'node:process'; +/** + * The Bun Runtime implementation + */ const BunRuntime: IRuntime = { name: RunTimeType.Bun, file: BunFileIO, diff --git a/src/common/fns.ts b/src/common/fns.ts index 912ccbb..9e95888 100644 --- a/src/common/fns.ts +++ b/src/common/fns.ts @@ -28,7 +28,7 @@ export function readKey(raw: Uint8Array): string { return parsed; } - // Some keycodes have multiple potential inputs + // Some key codes have multiple potential inputs switch (parsed) { case '\x1b[1~': case '\x1b[7~': diff --git a/src/common/option.ts b/src/common/option.ts index 867e03f..43a8603 100644 --- a/src/common/option.ts +++ b/src/common/option.ts @@ -13,7 +13,7 @@ enum OptionType { const isOption = (v: any): v is Option => v instanceof Option; -class OptionInnerNone { +class OptionInnerNone { public type: OptionType = OptionType.None; } @@ -23,7 +23,7 @@ class OptionInnerSome { constructor(public value: T) {} } -type OptionInnerType = OptionInnerNone | OptionInnerSome; +type OptionInnerType = OptionInnerNone | OptionInnerSome; const isSome = (v: OptionInnerType): v is OptionInnerSome => 'value' in v && v.type === OptionType.Some; @@ -50,6 +50,9 @@ export class Option { : new OptionInnerNone(); } + /** + * The equivalent of the Rust `Option`.`None` type + */ public static get None(): Option { return Option._None; } diff --git a/src/common/runtime.ts b/src/common/runtime.ts index 2e7fae6..36b89ac 100644 --- a/src/common/runtime.ts +++ b/src/common/runtime.ts @@ -1,3 +1,6 @@ +/** + * Functions/Methods that depend on the current runtime to function + */ import process from 'node:process'; import { IRuntime, ITestBase } from './types.ts'; import { noop } from './fns.ts'; @@ -11,9 +14,13 @@ export type { IFileIO, IRuntime, ITerminal } from './types.ts'; export enum RunTimeType { Bun = 'bun', Deno = 'deno', + Tsx = 'tsx', Unknown = 'common', } +/** + * The label for type/severity of the log entry + */ export enum LogLevel { Debug = 'Debug', Info = 'Info', @@ -62,7 +69,7 @@ export function die(s: string | Error): void { * Determine which Typescript runtime we are operating under */ export function runtimeType(): RunTimeType { - let runtime = RunTimeType.Unknown; + let runtime = RunTimeType.Tsx; if ('Deno' in globalThis) { runtime = RunTimeType.Deno; diff --git a/src/deno/mod.ts b/src/deno/mod.ts index c5a48f8..4fefcac 100644 --- a/src/deno/mod.ts +++ b/src/deno/mod.ts @@ -7,6 +7,9 @@ import DenoFileIO from './file_io.ts'; import * as node_process from 'node:process'; +/** + * The Deno Runtime implementation + */ const DenoRuntime: IRuntime = { name: RunTimeType.Deno, file: DenoFileIO, diff --git a/src/tsx/file_io.ts b/src/tsx/file_io.ts new file mode 100644 index 0000000..03b4040 --- /dev/null +++ b/src/tsx/file_io.ts @@ -0,0 +1,25 @@ +import { appendFile, readFile, writeFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; + +import { IFileIO } from '../common/runtime.ts'; + +const TsxFileIO: IFileIO = { + openFile: async function (path: string): Promise { + const filePath = resolve(path); + const contents = await readFile(filePath, { encoding: 'utf8' }); + + return contents; + }, + appendFile: async function (path: string, contents: string): Promise { + const filePath = resolve(path); + + await appendFile(filePath, contents); + }, + saveFile: async function (path: string, contents: string): Promise { + const filePath = resolve(path); + + await writeFile(filePath, contents); + }, +}; + +export default TsxFileIO; diff --git a/src/tsx/mod.ts b/src/tsx/mod.ts new file mode 100644 index 0000000..6635e68 --- /dev/null +++ b/src/tsx/mod.ts @@ -0,0 +1,26 @@ +/** + * The main entrypoint when using Tsx as the runtime + */ +import { IRuntime, RunTimeType } from '../common/runtime.ts'; +import TsxTerminalIO from './terminal_io.ts'; +import TsxFileIO from './file_io.ts'; + +import process from 'node:process'; + +/** + * The Tsx Runtime implementation + */ +const TsxRuntime: IRuntime = { + name: RunTimeType.Tsx, + file: TsxFileIO, + term: TsxTerminalIO, + onEvent: (eventName: string, handler) => process.on(eventName, handler), + onExit: (cb: () => void): void => { + process.on('beforeExit', cb); + process.on('exit', cb); + process.on('SIGINT', cb); + }, + exit: (code?: number) => process.exit(code), +}; + +export default TsxRuntime; diff --git a/src/tsx/terminal_io.ts b/src/tsx/terminal_io.ts new file mode 100644 index 0000000..020433a --- /dev/null +++ b/src/tsx/terminal_io.ts @@ -0,0 +1,42 @@ +import process from 'node:process'; + +import { readKey } from '../common/fns.ts'; +import { ITerminal, ITerminalSize } from '../common/types.ts'; + +const TsxTerminalIO: ITerminal = { + argv: (process.argv.length > 2) ? process.argv.slice(2) : [], + inputLoop: async function* (): AsyncGenerator { + yield (await TsxTerminalIO.readStdinRaw()) ?? new Uint8Array(0); + + return null; + }, + getTerminalSize: function getTerminalSize(): Promise { + const [cols, rows] = process.stdout.getWindowSize(); + + return Promise.resolve({ + rows, + cols, + }); + }, + readStdin: async function (): Promise { + const raw = await TsxTerminalIO.readStdinRaw(); + return readKey(raw ?? new Uint8Array(0)); + }, + readStdinRaw: function (): Promise { + return new Promise((resolve) => { + process.stdin.setRawMode(true).resume().once( + 'data', + (buffer: Uint8Array) => { + resolve(buffer); + process.stdin.setRawMode(false); + }, + ); + }); + }, + writeStdout: function (s: string): Promise { + process.stdout.write(s); + return Promise.resolve(); + }, +}; + +export default TsxTerminalIO; diff --git a/src/tsx/test_base.ts b/src/tsx/test_base.ts new file mode 100644 index 0000000..2bb3ab0 --- /dev/null +++ b/src/tsx/test_base.ts @@ -0,0 +1,41 @@ +/** + * Adapt the node test interface to the shared testing interface + */ +// @ts-ignore: Only in newer versions of node +import { + deepStrictEqual, + notStrictEqual, + strictEqual, +} from 'node:assert/strict'; +// @ts-ignore: Only in newer versions of node +import { describe, it } from 'node:test'; +import { ITestBase } from '../common/types.ts'; + +export function testSuite(testObj: any) { + Object.keys(testObj).forEach((group) => { + describe(group, () => { + const groupObj = testObj[group]; + Object.keys(groupObj).forEach((testName) => { + it(testName, groupObj[testName]); + }); + }); + }); +} + +const TsxTestBase: ITestBase = { + assertEquals: (actual: unknown, expected: unknown) => + deepStrictEqual(actual, expected), + assertExists: (actual: unknown) => notStrictEqual(actual, undefined), + assertFalse: (actual: boolean) => strictEqual(actual, false), + assertInstanceOf: (actual: unknown, expectedType: any) => + strictEqual(actual instanceof expectedType, true), + assertNotEquals: (actual: unknown, expected: unknown) => + notStrictEqual(actual, expected), + assertNull: (actual: unknown) => strictEqual(actual, null), + assertStrictEquals: (actual: unknown, expected: unknown) => + strictEqual(actual, expected), + assertTrue: (actual: boolean) => strictEqual(actual, true), + testSuite, +}; + +export default TsxTestBase;