From 6f121f71729731de5f54c668e1b435d6a7b1e19a Mon Sep 17 00:00:00 2001 From: Vincent LE GOFF Date: Tue, 12 Nov 2019 21:51:14 +0100 Subject: [PATCH] feat: node (denoland/deno#3319) --- node/README.md | 7 +++ node/_utils.ts | 35 ++++++++++++ node/fs.ts | 68 ++++++++++++++++++++++ node/fs_test.ts | 47 ++++++++++++++++ node/testdata/hello.txt | 1 + node/util.ts | 47 ++++++++++++++++ node/util_test.ts | 122 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 327 insertions(+) create mode 100644 node/README.md create mode 100644 node/_utils.ts create mode 100644 node/fs.ts create mode 100644 node/fs_test.ts create mode 100644 node/testdata/hello.txt create mode 100644 node/util.ts create mode 100644 node/util_test.ts diff --git a/node/README.md b/node/README.md new file mode 100644 index 000000000000..6d363a11dd55 --- /dev/null +++ b/node/README.md @@ -0,0 +1,7 @@ +# Deno Node compatibility + +This module is meant to have a compatibility layer for the +[nodeJS standard library](https://nodejs.org/docs/latest-v12.x/api/). + +**Warning** : Any function of this module should not be referred anywhere in the +deno standard library as it's a compatiblity module. diff --git a/node/_utils.ts b/node/_utils.ts new file mode 100644 index 000000000000..f0808e82b1dd --- /dev/null +++ b/node/_utils.ts @@ -0,0 +1,35 @@ +export function notImplemented(msg?: string): never { + const message = msg ? `Not implemented: ${msg}` : "Not implemented"; + throw new Error(message); +} + +// API helpers + +export type MaybeNull = T | null; +export type MaybeDefined = T | undefined; +export type MaybeEmpty = T | null | undefined; + +export function intoCallbackAPI( + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + func: (...args: any[]) => Promise, + cb: MaybeEmpty<(err: MaybeNull, value: MaybeEmpty) => void>, + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + ...args: any[] +): void { + func(...args) + .then(value => cb && cb(null, value)) + .catch(err => cb && cb(err, null)); +} + +export function intoCallbackAPIWithIntercept( + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + func: (...args: any[]) => Promise, + interceptor: (v: T1) => T2, + cb: MaybeEmpty<(err: MaybeNull, value: MaybeEmpty) => void>, + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + ...args: any[] +): void { + func(...args) + .then(value => cb && cb(null, interceptor(value))) + .catch(err => cb && cb(err, null)); +} diff --git a/node/fs.ts b/node/fs.ts new file mode 100644 index 000000000000..01b9e23989ee --- /dev/null +++ b/node/fs.ts @@ -0,0 +1,68 @@ +import { notImplemented, intoCallbackAPIWithIntercept } from "./_utils.ts"; +const { readFile: denoReadFile, readFileSync: denoReadFileSync } = Deno; + +type ReadFileCallback = (err: Error | null, data: string | Uint8Array) => void; + +interface ReadFileOptions { + encoding?: string | null; + flag?: string; +} + +function getEncoding( + optOrCallback?: ReadFileOptions | ReadFileCallback +): string | null { + if (!optOrCallback || typeof optOrCallback === "function") { + return null; + } else { + if (optOrCallback.encoding) { + if ( + optOrCallback.encoding === "utf8" || + optOrCallback.encoding === "utf-8" + ) { + return "utf8"; + } else { + notImplemented(); + } + } + return null; + } +} + +function maybeDecode( + data: Uint8Array, + encoding: string | null +): string | Uint8Array { + if (encoding === "utf8") { + return new TextDecoder().decode(data); + } + return data; +} + +export function readFile( + path: string, + optOrCallback: ReadFileCallback | ReadFileOptions, + callback?: ReadFileCallback +): void { + let cb: ReadFileCallback | undefined; + if (typeof optOrCallback === "function") { + cb = optOrCallback; + } else { + cb = callback; + } + + const encoding = getEncoding(optOrCallback); + + intoCallbackAPIWithIntercept( + denoReadFile, + (data: Uint8Array): string | Uint8Array => maybeDecode(data, encoding), + cb, + path + ); +} + +export function readFileSync( + path: string, + opt?: ReadFileOptions +): string | Uint8Array { + return maybeDecode(denoReadFileSync(path), getEncoding(opt)); +} diff --git a/node/fs_test.ts b/node/fs_test.ts new file mode 100644 index 000000000000..81b9594f1392 --- /dev/null +++ b/node/fs_test.ts @@ -0,0 +1,47 @@ +import { readFile, readFileSync } from "./fs.ts"; +import { test } from "../testing/mod.ts"; +import * as path from "../path/mod.ts"; +import { assertEquals, assert } from "../testing/asserts.ts"; + +const testData = path.resolve(path.join("node", "testdata", "hello.txt")); + +// Need to convert to promises, otherwise test() won't report error correctly. +test(async function readFileSuccess() { + const data = await new Promise((res, rej) => { + readFile(testData, (err, data) => { + if (err) { + rej(err); + } + res(data); + }); + }); + + assert(data instanceof Uint8Array); + assertEquals(new TextDecoder().decode(data as Uint8Array), "hello world"); +}); + +test(async function readFileEncodeUtf8Success() { + const data = await new Promise((res, rej) => { + readFile(testData, { encoding: "utf8" }, (err, data) => { + if (err) { + rej(err); + } + res(data); + }); + }); + + assertEquals(typeof data, "string"); + assertEquals(data as string, "hello world"); +}); + +test(function readFileSyncSuccess() { + const data = readFileSync(testData); + assert(data instanceof Uint8Array); + assertEquals(new TextDecoder().decode(data as Uint8Array), "hello world"); +}); + +test(function readFileEncodeUtf8Success() { + const data = readFileSync(testData, { encoding: "utf8" }); + assertEquals(typeof data, "string"); + assertEquals(data as string, "hello world"); +}); diff --git a/node/testdata/hello.txt b/node/testdata/hello.txt new file mode 100644 index 000000000000..95d09f2b1015 --- /dev/null +++ b/node/testdata/hello.txt @@ -0,0 +1 @@ +hello world \ No newline at end of file diff --git a/node/util.ts b/node/util.ts new file mode 100644 index 000000000000..d0187e6160da --- /dev/null +++ b/node/util.ts @@ -0,0 +1,47 @@ +export function isArray(value: unknown): boolean { + return Array.isArray(value); +} + +export function isBoolean(value: unknown): boolean { + return typeof value === "boolean" || value instanceof Boolean; +} + +export function isNull(value: unknown): boolean { + return value === null; +} + +export function isNullOrUndefined(value: unknown): boolean { + return value === null || value === undefined; +} + +export function isNumber(value: unknown): boolean { + return typeof value === "number" || value instanceof Number; +} + +export function isString(value: unknown): boolean { + return typeof value === "string" || value instanceof String; +} + +export function isSymbol(value: unknown): boolean { + return typeof value === "symbol"; +} + +export function isUndefined(value: unknown): boolean { + return value === undefined; +} + +export function isObject(value: unknown): boolean { + return value !== null && typeof value === "object"; +} + +export function isError(e: unknown): boolean { + return e instanceof Error; +} + +export function isFunction(value: unknown): boolean { + return typeof value === "function"; +} + +export function isRegExp(value: unknown): boolean { + return value instanceof RegExp; +} diff --git a/node/util_test.ts b/node/util_test.ts new file mode 100644 index 000000000000..19512bedb468 --- /dev/null +++ b/node/util_test.ts @@ -0,0 +1,122 @@ +import { test } from "../testing/mod.ts"; +import { assert } from "../testing/asserts.ts"; +import * as util from "./util.ts"; + +test({ + name: "[util] isBoolean", + fn() { + assert(util.isBoolean(true)); + assert(util.isBoolean(new Boolean())); + assert(util.isBoolean(new Boolean(true))); + assert(util.isBoolean(false)); + assert(!util.isBoolean("deno")); + assert(!util.isBoolean("true")); + } +}); + +test({ + name: "[util] isNull", + fn() { + let n; + assert(util.isNull(null)); + assert(!util.isNull(n)); + assert(!util.isNull(0)); + assert(!util.isNull({})); + } +}); + +test({ + name: "[util] isNullOrUndefined", + fn() { + let n; + assert(util.isNullOrUndefined(null)); + assert(util.isNullOrUndefined(n)); + assert(!util.isNullOrUndefined({})); + assert(!util.isNullOrUndefined("undefined")); + } +}); + +test({ + name: "[util] isNumber", + fn() { + assert(util.isNumber(666)); + assert(util.isNumber(new Number(666))); + assert(!util.isNumber("999")); + assert(!util.isNumber(null)); + } +}); + +test({ + name: "[util] isString", + fn() { + assert(util.isString("deno")); + assert(util.isString(new String("DIO"))); + assert(!util.isString(1337)); + } +}); + +test({ + name: "[util] isSymbol", + fn() {} +}); + +test({ + name: "[util] isUndefined", + fn() { + let t; + assert(util.isUndefined(t)); + assert(!util.isUndefined("undefined")); + assert(!util.isUndefined({})); + } +}); + +test({ + name: "[util] isObject", + fn() { + const dio = { stand: "Za Warudo" }; + assert(util.isObject(dio)); + assert(util.isObject(new RegExp(/Toki Wo Tomare/))); + assert(!util.isObject("Jotaro")); + } +}); + +test({ + name: "[util] isError", + fn() { + const java = new Error(); + const nodejs = new TypeError(); + const deno = "Future"; + assert(util.isError(java)); + assert(util.isError(nodejs)); + assert(!util.isError(deno)); + } +}); + +test({ + name: "[util] isFunction", + fn() { + const f = function(): void {}; + assert(util.isFunction(f)); + assert(!util.isFunction({})); + assert(!util.isFunction(new RegExp(/f/))); + } +}); + +test({ + name: "[util] isRegExp", + fn() { + assert(util.isRegExp(new RegExp(/f/))); + assert(util.isRegExp(/fuManchu/)); + assert(!util.isRegExp({ evil: "eye" })); + assert(!util.isRegExp(null)); + } +}); + +test({ + name: "[util] isArray", + fn() { + assert(util.isArray([])); + assert(!util.isArray({ yaNo: "array" })); + assert(!util.isArray(null)); + } +});