Socket
Socket
Sign inDemoInstall

@ghom/handler

Package Overview
Dependencies
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ghom/handler - npm Package Compare versions

Comparing version 2.0.0 to 3.0.0

tests/files/a.txt

44

dist/app/handler.d.ts

@@ -1,2 +0,5 @@

export interface HandlerOptions<Element> {
/// <reference types="node" />
/// <reference types="node" />
import fs from "fs";
export interface HandlerOptions<Data> {
logger?: {

@@ -17,18 +20,35 @@ log: (message: string) => void;

loggerPattern?: string;
loader?: (path: string) => Promise<Element>;
loader?: (path: string) => Promise<Data>;
/**
* If this function is defined, the reloaded files will be loaded by this function instead of the loader
*/
reloader?: (path: string) => Promise<Data>;
pattern?: RegExp;
onLoad?: (path: string) => Promise<void>;
onLoad?: (path: string, data?: Data) => Promise<void>;
/**
* If this function is defined, the reloaded files will stop
* going through the onLoad function and go through this one instead
*/
onReload?: (path: string, data?: Data) => Promise<void>;
onFinish?: (paths: string[]) => Promise<void>;
/**
* @default false
*/
hotReload?: boolean;
/**
* @default 100
*/
hotReloadTimeout?: number;
}
export declare class Handler<Element> {
export declare class Handler<Data> {
private path;
private options?;
elements: Map<string, Element>;
constructor(path: string, options?: HandlerOptions<Element> | undefined);
/**
* Here to prevent breaking changes.
* @deprecated Use `load` instead.
*/
load(): Promise<void>;
init(): Promise<void>;
elements: Map<string, Data>;
md5: Map<string, string>;
timeouts: Map<string, NodeJS.Timeout | false>;
watcher?: fs.FSWatcher;
constructor(path: string, options?: HandlerOptions<Data> | undefined);
init(this: this): Promise<void>;
destroy(this: this): void;
private _handle;
}

@@ -8,3 +8,4 @@ "use strict";

const path_1 = __importDefault(require("path"));
const promises_1 = __importDefault(require("fs/promises"));
const md5_1 = __importDefault(require("md5"));
const fs_1 = __importDefault(require("fs"));
class Handler {

@@ -15,34 +16,84 @@ constructor(path, options) {

this.elements = new Map();
this.md5 = new Map();
this.timeouts = new Map();
}
/**
* Here to prevent breaking changes.
* @deprecated Use `load` instead.
*/
async load() {
await this.init();
}
async init() {
this.elements.clear();
const filenames = await promises_1.default.readdir(this.path);
const filenames = await fs_1.default.promises.readdir(this.path);
const filepathList = [];
for (const basename of filenames) {
if (this.options?.pattern && !this.options.pattern.test(basename))
continue;
const filepath = path_1.default.join(this.path, basename);
const filename = path_1.default.basename(filepath, path_1.default.extname(filepath));
filepathList.push(filepath);
if (this.options?.logger)
this.options.logger.log(this.options.loggerPattern
? this.options.loggerPattern
.replace("$path", filepath)
.replace("$basename", basename)
.replace("$filename", filename)
: `loaded ${filename}`);
if (this.options?.loader)
this.elements.set(filepath, await this.options.loader(filepath));
await this.options?.onLoad?.(filepath);
try {
filepathList.push(await this._handle(basename, false));
}
catch (error) {
if (error.message.startsWith("Ignored"))
continue;
else
throw error;
}
}
await this.options?.onFinish?.(filepathList);
if (this.options?.hotReload)
this.watcher = fs_1.default.watch(this.path, async (event, basename) => {
if (event !== "change" || !basename)
return;
try {
await this._handle(basename, true);
}
catch (error) {
if (error.message.startsWith("Ignored"))
return;
else
throw error;
}
});
}
destroy() {
this.watcher?.close();
this.timeouts.forEach((timeout) => timeout && clearTimeout(timeout));
this.timeouts.clear();
this.elements.clear();
this.md5.clear();
}
async _handle(basename, reloaded) {
if (this.options?.pattern && !this.options.pattern.test(basename))
throw new Error(`Ignored ${basename} by pattern`);
const filepath = path_1.default.join(this.path, basename);
const filename = path_1.default.basename(filepath, path_1.default.extname(filepath));
if (this.options?.hotReload) {
if (this.timeouts.get(filepath))
throw new Error(`Ignored ${basename} by timeout`);
this.timeouts.set(filepath, setTimeout(() => {
this.timeouts.set(filepath, false);
}, this.options.hotReloadTimeout ?? 100));
const md5sum = (0, md5_1.default)(fs_1.default.readFileSync(filepath));
if (this.md5.get(filepath) === md5sum)
throw new Error(`Ignored ${basename} by md5 check`);
else
this.md5.set(filepath, md5sum);
}
if (this.options?.logger)
this.options.logger.log(this.options.loggerPattern
? this.options.loggerPattern
.replace("$path", filepath)
.replace("$basename", basename)
.replace("$filename", filename)
: `loaded ${filename}`);
let loaded;
const loader = reloaded
? this.options?.reloader ?? this.options?.loader
: this.options?.loader;
const onLoad = reloaded
? this.options?.onReload ?? this.options?.onLoad
: this.options?.onLoad;
if (loader) {
loaded = await loader(filepath);
this.elements.set(filepath, loaded);
}
if (onLoad) {
await onLoad(filepath, loaded);
}
return filepath;
}
}
exports.Handler = Handler;
{
"name": "@ghom/handler",
"version": "2.0.0",
"version": "3.0.0",
"license": "MIT",

@@ -12,13 +12,17 @@ "main": "dist/index.js",

"scripts": {
"format": "prettier --write src tsconfig.*",
"format": "prettier --write src tsconfig.* tests",
"build": "tsc",
"test": "npm run build && jest tests/test.js",
"test": "npm run build && jest tests/test.js --detectOpenHandles",
"prepublishOnly": "npm run format && npm test"
},
"devDependencies": {
"@types/jest": "^29.5.11",
"@types/md5": "^2.3.5",
"jest": "^29.7.0",
"prettier": "^2.5.1",
"typescript": "^4.5.5",
"jest": "^29.7.0",
"@types/jest": "^29.5.5"
"typescript": "^4.5.5"
},
"dependencies": {
"md5": "^2.3.0"
}
}
# File handler
## Example for simple table handler
## Basic usage

@@ -8,11 +8,10 @@ ```ts

export const handler = new Handler("dist/table", {
logger: console,
loggerPattern: "loaded new table: $filename",
loader: (path) => import(`file://${path}`),
pattern: /\.js$/,
})
const handler = new Handler(" ... ")
try {
// For load the files, call this function:
await handler.init()
// You can access the loaded files with the "elements" property:
console.log(handler.elements)
} catch(e) {

@@ -22,3 +21,68 @@ console.error(e)

console.log(handler.elements)
// For terminate the hot reloading and empty the handler cache, call this function:
handler.destroy()
```
## Config example for simple JS module handler
```ts
export const handler = new Handler("dist/files", {
logger: console,
loggerPattern: "loaded new table: $filename",
loader: (path) => import(`file://${path}`),
pattern: /\.js$/,
})
```
## Config example for js module handler with hot reloading
```ts
export const handler = new Handler("dist/files", {
pattern: /\.js$/,
hotReload: true,
loader: (path) => import(`file://${path}`),
reloader: (path) => import(`file://${path}?update=${Date.now()}`),
onLoad: (path, data) => {
// Do something with the loaded data
},
onReload: (path, data) => {
// Do something with the reloaded data
},
})
```
The `?update=${Date.now()}` text part is important if you want to import the changed file with the hot reloading.
## Same example but with CommonJS
```ts
const handler = new Handler("dist/table", {
pattern: /\.js$/,
hotReload: true,
loader: (path) => {
return require(path)
},
reloader: (path) => {
delete require.cache[require.resolve(path)]
return require(path)
},
// ...
})
```
The `delete require.cache[require.resolve(path)]` line is important if you want to require the changed file with the hot reloading.
## Example for simple file handler
```ts
import fs from "fs"
const handler = new Handler(path.join(__dirname, "files"), {
pattern: /\.txt$/i,
hotReload: true,
loader: async (filepath) => {
return fs.promises.readFile(filepath, "utf8")
},
})
```
import path from "path"
import fs from "fs/promises"
import md5 from "md5"
import fs from "fs"
export interface HandlerOptions<Element> {
export interface HandlerOptions<Data> {
logger?: {

@@ -20,56 +21,133 @@ log: (message: string) => void

loggerPattern?: string
loader?: (path: string) => Promise<Element>
loader?: (path: string) => Promise<Data>
/**
* If this function is defined, the reloaded files will be loaded by this function instead of the loader
*/
reloader?: (path: string) => Promise<Data>
pattern?: RegExp
onLoad?: (path: string) => Promise<void>
onLoad?: (path: string, data?: Data) => Promise<void>
/**
* If this function is defined, the reloaded files will stop
* going through the onLoad function and go through this one instead
*/
onReload?: (path: string, data?: Data) => Promise<void>
onFinish?: (paths: string[]) => Promise<void>
/**
* @default false
*/
hotReload?: boolean
/**
* @default 100
*/
hotReloadTimeout?: number
}
export class Handler<Element> {
public elements: Map<string, Element> = new Map()
export class Handler<Data> {
public elements: Map<string, Data> = new Map()
public md5: Map<string, string> = new Map()
public timeouts: Map<string, NodeJS.Timeout | false> = new Map()
public watcher?: fs.FSWatcher
public constructor(
private path: string,
private options?: HandlerOptions<Element>
private options?: HandlerOptions<Data>
) {}
/**
* Here to prevent breaking changes.
* @deprecated Use `load` instead.
*/
async load() {
await this.init()
}
async init() {
async init(this: this) {
this.elements.clear()
const filenames = await fs.readdir(this.path)
const filenames = await fs.promises.readdir(this.path)
const filepathList: string[] = []
for (const basename of filenames) {
if (this.options?.pattern && !this.options.pattern.test(basename))
continue
try {
filepathList.push(await this._handle(basename, false))
} catch (error: any) {
if (error.message.startsWith("Ignored")) continue
else throw error
}
}
const filepath = path.join(this.path, basename)
const filename = path.basename(filepath, path.extname(filepath))
await this.options?.onFinish?.(filepathList)
filepathList.push(filepath)
if (this.options?.hotReload)
this.watcher = fs.watch(this.path, async (event, basename) => {
if (event !== "change" || !basename) return
if (this.options?.logger)
this.options.logger.log(
this.options.loggerPattern
? this.options.loggerPattern
.replace("$path", filepath)
.replace("$basename", basename)
.replace("$filename", filename)
: `loaded ${filename}`
)
try {
await this._handle(basename, true)
} catch (error: any) {
if (error.message.startsWith("Ignored")) return
else throw error
}
})
}
if (this.options?.loader)
this.elements.set(filepath, await this.options.loader(filepath))
destroy(this: this) {
this.watcher?.close()
this.timeouts.forEach((timeout) => timeout && clearTimeout(timeout))
this.timeouts.clear()
this.elements.clear()
this.md5.clear()
}
await this.options?.onLoad?.(filepath)
private async _handle(
this: this,
basename: string,
reloaded: boolean
): Promise<string> {
if (this.options?.pattern && !this.options.pattern.test(basename))
throw new Error(`Ignored ${basename} by pattern`)
const filepath = path.join(this.path, basename)
const filename = path.basename(filepath, path.extname(filepath))
if (this.options?.hotReload) {
if (this.timeouts.get(filepath))
throw new Error(`Ignored ${basename} by timeout`)
this.timeouts.set(
filepath,
setTimeout(() => {
this.timeouts.set(filepath, false)
}, this.options.hotReloadTimeout ?? 100)
)
const md5sum = md5(fs.readFileSync(filepath))
if (this.md5.get(filepath) === md5sum)
throw new Error(`Ignored ${basename} by md5 check`)
else this.md5.set(filepath, md5sum)
}
await this.options?.onFinish?.(filepathList)
if (this.options?.logger)
this.options.logger.log(
this.options.loggerPattern
? this.options.loggerPattern
.replace("$path", filepath)
.replace("$basename", basename)
.replace("$filename", filename)
: `loaded ${filename}`
)
let loaded!: Data
const loader = reloaded
? this.options?.reloader ?? this.options?.loader
: this.options?.loader
const onLoad = reloaded
? this.options?.onReload ?? this.options?.onLoad
: this.options?.onLoad
if (loader) {
loaded = await loader(filepath)
this.elements.set(filepath, loaded)
}
if (onLoad) {
await onLoad(filepath, loaded)
}
return filepath
}
}
const path = require("path")
const fs = require("fs")
const { Handler } = require("../dist/index")
test("load", (done) => {
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const Inputs = jest.fn().mockReturnValueOnce("42").mockReturnValueOnce("666")
const Loaded = jest
.fn()
.mockReturnValueOnce("0")
.mockReturnValueOnce("1")
.mockReturnValueOnce("2")
const Reloaded = jest.fn().mockReturnValueOnce("42").mockReturnValueOnce("666")
beforeAll(() => {
fs.writeFileSync(path.join(__dirname, "files", "b.txt"), "1")
})
test("test", (done) => {
const handler = new Handler(path.join(__dirname, "files"), {
pattern: /\.js$/i,
onLoad: (filepath) => {
const file = require(filepath)
expect(typeof file === "number" && file > -1 && file < 3).toBeTruthy()
}
pattern: /\.txt$/i,
hotReload: true,
loader: async (filepath) => {
return fs.promises.readFile(filepath, "utf8")
},
onLoad: async (filepath, data) => {
expect(data).toBe(Loaded())
},
onReload: async (filepath, data) => {
expect(data).toBe(Reloaded())
},
})
handler.init().then(done).catch(done)
const tick = async () => {
fs.writeFileSync(path.join(__dirname, "files", "b.txt"), Inputs())
await wait(150)
}
handler
.init()
.then(() => wait(150))
.then(tick)
.then(tick)
.then(() => {
handler.destroy()
done()
})
.catch(done)
})
afterAll(() => {
fs.writeFileSync(path.join(__dirname, "files", "b.txt"), "1")
})
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc