Socket
Socket
Sign inDemoInstall

@ghom/handler

Package Overview
Dependencies
19
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 3.0.0 to 3.1.0

.gitattributes

65

dist/app/handler.d.ts

@@ -1,5 +0,24 @@

/// <reference types="node" />
/// <reference types="node" />
import fs from "fs";
import chokidar from "chokidar";
export interface HandlerOptions<Data> {
/**
* File basename pattern to filter files
*/
pattern?: RegExp;
/**
* @default false
*/
hotReload?: boolean;
/**
* @default 100
*/
hotReloadTimeout?: number;
/**
* If you want to log the file loading process, you can set up a logger. <br>
* The logger must have a log method that accepts a string as a parameter. <br>
* @example ```ts
* const handler = new Handler("./commands", {
* logger: console
* })
* ```
*/
logger?: {

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

loggerPattern?: string;
loader?: (path: string) => Promise<Data>;
/**
* If this function is defined, the reloaded files will be loaded by this function instead of the loader
* This method will load the file and return the data. <br>
* The data will be stored in the {@link Handler.elements} map.
*/
reloader?: (path: string) => Promise<Data>;
pattern?: RegExp;
onLoad?: (path: string, data?: Data) => Promise<void>;
loader: (path: string) => Promise<Data>;
/**
* If this function is defined, the reloaded files will stop
* going through the onLoad function and go through this one instead
* This method will be called when the file is loaded. <br>
* The data will be transferred from the {@link loader} method.
*/
onReload?: (path: string, data?: Data) => Promise<void>;
onFinish?: (paths: string[]) => Promise<void>;
onLoad?: (path: string, data: Data) => Promise<void>;
/**
* @default false
* If this function is defined, the changed files will stop
* going through the {@link onLoad} function and go through this one instead
*/
hotReload?: boolean;
onChange?: (path: string, data: Data) => Promise<void>;
/**
* @default 100
* This method will be called when the file is removed.
*/
hotReloadTimeout?: number;
onRemove?: (path: string, oldData: Data) => Promise<void>;
/**
* This method will be called after all files are loaded, at the end of the {@link Handler.init} method.
*/
onFinish?: (data: Map<string, Data>) => Promise<void>;
}
export declare class Handler<Data> {
private path;
private options?;
private dirname;
private options;
elements: Map<string, Data>;
md5: Map<string, string>;
timeouts: Map<string, NodeJS.Timeout | false>;
watcher?: fs.FSWatcher;
constructor(path: string, options?: HandlerOptions<Data> | undefined);
watcher?: chokidar.FSWatcher;
constructor(dirname: string, options: HandlerOptions<Data>);
init(this: this): Promise<void>;
destroy(this: this): void;
private _handle;
private _load;
private _remove;
}

@@ -10,17 +10,17 @@ "use strict";

const fs_1 = __importDefault(require("fs"));
const chokidar_1 = __importDefault(require("chokidar"));
class Handler {
constructor(path, options) {
this.path = path;
constructor(dirname, options) {
this.dirname = dirname;
this.options = options;
this.elements = new Map();
this.md5 = new Map();
this.timeouts = new Map();
}
async init() {
this.elements.clear();
const filenames = await fs_1.default.promises.readdir(this.path);
const filepathList = [];
const filenames = await fs_1.default.promises.readdir(this.dirname);
for (const basename of filenames) {
const filepath = path_1.default.join(this.dirname, basename);
try {
filepathList.push(await this._handle(basename, false));
await this._load(filepath, false);
}

@@ -34,9 +34,17 @@ catch (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;
await this.options.onFinish?.(this.elements);
if (this.options.hotReload)
this.watcher = chokidar_1.default
.watch(path_1.default.join(this.dirname, "*.*"))
.on("all", async (event, filepath) => {
try {
await this._handle(basename, true);
switch (event) {
case "add":
case "change":
await this._load(filepath, event === "change");
break;
case "unlink":
await this._remove(filepath);
break;
}
}

@@ -53,18 +61,11 @@ catch (error) {

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))
async _load(filepath, reloaded) {
const basename = path_1.default.basename(filepath);
const filename = path_1.default.basename(filepath, path_1.default.extname(filepath));
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));
if (this.options.hotReload) {
const md5sum = (0, md5_1.default)(fs_1.default.readFileSync(filepath));

@@ -76,3 +77,3 @@ if (this.md5.get(filepath) === md5sum)

}
if (this.options?.logger)
if (this.options.logger)
this.options.logger.log(this.options.loggerPattern

@@ -85,18 +86,31 @@ ? this.options.loggerPattern

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);
}
? this.options.onChange ?? this.options.onLoad
: this.options.onLoad;
loaded = await this.options.loader(filepath);
this.elements.set(filepath, loaded);
if (onLoad) {
await onLoad(filepath, loaded);
}
return filepath;
}
async _remove(filepath) {
const basename = path_1.default.basename(filepath);
const filename = path_1.default.basename(filepath, path_1.default.extname(filepath));
if (this.options.pattern && !this.options.pattern.test(basename))
throw new Error(`Ignored ${basename} by pattern`);
if (!this.elements.has(filepath))
throw new Error(`Ignored ${basename} because isn't loaded`);
if (this.options.logger)
this.options.logger.log(this.options.loggerPattern
? this.options.loggerPattern
.replace("$path", filepath)
.replace("$basename", basename)
.replace("$filename", filename)
: `removed ${filename}`);
const data = this.elements.get(filepath);
this.elements.delete(filepath);
this.md5.delete(filepath);
await this.options.onRemove?.(filepath, data);
}
}
exports.Handler = Handler;
{
"name": "@ghom/handler",
"version": "3.0.0",
"version": "3.1.0",
"license": "MIT",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"description": "File handler that extends @ghom/event-emitter",
"description": "File handler with real hot-reload for Node.js",
"prettier": {
"semi": false
"semi": false,
"endOfLine": "lf"
},

@@ -25,4 +26,5 @@ "scripts": {

"dependencies": {
"chokidar": "^3.5.3",
"md5": "^2.3.0"
}
}
# File handler
This package is a file handler for NodeJS. It can load files from a directory, and it can reload the files if they changed.
You can handle any file type with this handler, but you need to write a loader function for it.
## Basic usage

@@ -42,9 +45,8 @@

hotReload: true,
loader: (path) => import(`file://${path}`),
reloader: (path) => import(`file://${path}?update=${Date.now()}`),
loader: (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
onChange: (path, data) => {
// Do something with the changed data
},

@@ -59,9 +61,6 @@ })

```ts
const handler = new Handler("dist/table", {
const handler = new Handler("dist/files", {
pattern: /\.js$/,
hotReload: true,
loader: (path) => {
return require(path)
},
reloader: (path) => {
delete require.cache[require.resolve(path)]

@@ -76,3 +75,3 @@ return require(path)

## Example for simple file handler
## Example for simple file handler (for txt files)

@@ -82,3 +81,3 @@ ```ts

const handler = new Handler(path.join(__dirname, "files"), {
const handler = new Handler("dist/files", {
pattern: /\.txt$/i,

@@ -85,0 +84,0 @@ hotReload: true,

import path from "path"
import md5 from "md5"
import fs from "fs"
import chokidar from "chokidar"
export interface HandlerOptions<Data> {
/**
* File basename pattern to filter files
*/
pattern?: RegExp
/**
* @default false
*/
hotReload?: boolean
/**
* @default 100
*/
hotReloadTimeout?: number
/**
* If you want to log the file loading process, you can set up a logger. <br>
* The logger must have a log method that accepts a string as a parameter. <br>
* @example ```ts
* const handler = new Handler("./commands", {
* logger: console
* })
* ```
*/
logger?: {

@@ -21,23 +43,25 @@ log: (message: string) => void

loggerPattern?: string
loader?: (path: string) => Promise<Data>
/**
* If this function is defined, the reloaded files will be loaded by this function instead of the loader
* This method will load the file and return the data. <br>
* The data will be stored in the {@link Handler.elements} map.
*/
reloader?: (path: string) => Promise<Data>
pattern?: RegExp
onLoad?: (path: string, data?: Data) => Promise<void>
loader: (path: string) => Promise<Data>
/**
* If this function is defined, the reloaded files will stop
* going through the onLoad function and go through this one instead
* This method will be called when the file is loaded. <br>
* The data will be transferred from the {@link loader} method.
*/
onReload?: (path: string, data?: Data) => Promise<void>
onFinish?: (paths: string[]) => Promise<void>
onLoad?: (path: string, data: Data) => Promise<void>
/**
* @default false
* If this function is defined, the changed files will stop
* going through the {@link onLoad} function and go through this one instead
*/
hotReload?: boolean
onChange?: (path: string, data: Data) => Promise<void>
/**
* @default 100
* This method will be called when the file is removed.
*/
hotReloadTimeout?: number
onRemove?: (path: string, oldData: Data) => Promise<void>
/**
* This method will be called after all files are loaded, at the end of the {@link Handler.init} method.
*/
onFinish?: (data: Map<string, Data>) => Promise<void>
}

@@ -48,8 +72,7 @@

public md5: Map<string, string> = new Map()
public timeouts: Map<string, NodeJS.Timeout | false> = new Map()
public watcher?: fs.FSWatcher
public watcher?: chokidar.FSWatcher
public constructor(
private path: string,
private options?: HandlerOptions<Data>
private dirname: string,
private options: HandlerOptions<Data>
) {}

@@ -60,8 +83,9 @@

const filenames = await fs.promises.readdir(this.path)
const filepathList: string[] = []
const filenames = await fs.promises.readdir(this.dirname)
for (const basename of filenames) {
const filepath = path.join(this.dirname, basename)
try {
filepathList.push(await this._handle(basename, false))
await this._load(filepath, false)
} catch (error: any) {

@@ -73,15 +97,23 @@ if (error.message.startsWith("Ignored")) continue

await this.options?.onFinish?.(filepathList)
await this.options.onFinish?.(this.elements)
if (this.options?.hotReload)
this.watcher = fs.watch(this.path, async (event, basename) => {
if (event !== "change" || !basename) return
try {
await this._handle(basename, true)
} catch (error: any) {
if (error.message.startsWith("Ignored")) return
else throw error
}
})
if (this.options.hotReload)
this.watcher = chokidar
.watch(path.join(this.dirname, "*.*"))
.on("all", async (event, filepath) => {
try {
switch (event) {
case "add":
case "change":
await this._load(filepath, event === "change")
break
case "unlink":
await this._remove(filepath)
break
}
} catch (error: any) {
if (error.message.startsWith("Ignored")) return
else throw error
}
})
}

@@ -91,4 +123,2 @@

this.watcher?.close()
this.timeouts.forEach((timeout) => timeout && clearTimeout(timeout))
this.timeouts.clear()
this.elements.clear()

@@ -98,24 +128,14 @@ this.md5.clear()

private async _handle(
private async _load(
this: this,
basename: string,
filepath: 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)
): Promise<void> {
const basename = path.basename(filepath)
const filename = path.basename(filepath, path.extname(filepath))
if (this.options?.hotReload) {
if (this.timeouts.get(filepath))
throw new Error(`Ignored ${basename} by timeout`)
if (this.options.pattern && !this.options.pattern.test(basename))
throw new Error(`Ignored ${basename} by pattern`)
this.timeouts.set(
filepath,
setTimeout(() => {
this.timeouts.set(filepath, false)
}, this.options.hotReloadTimeout ?? 100)
)
if (this.options.hotReload) {
const md5sum = md5(fs.readFileSync(filepath))

@@ -128,3 +148,3 @@

if (this.options?.logger)
if (this.options.logger)
this.options.logger.log(

@@ -141,13 +161,8 @@ this.options.loggerPattern

const loader = reloaded
? this.options?.reloader ?? this.options?.loader
: this.options?.loader
const onLoad = reloaded
? this.options?.onReload ?? this.options?.onLoad
: this.options?.onLoad
? this.options.onChange ?? this.options.onLoad
: this.options.onLoad
if (loader) {
loaded = await loader(filepath)
this.elements.set(filepath, loaded)
}
loaded = await this.options.loader(filepath)
this.elements.set(filepath, loaded)

@@ -157,5 +172,31 @@ if (onLoad) {

}
}
return filepath
private async _remove(this: this, filepath: string): Promise<void> {
const basename = path.basename(filepath)
const filename = path.basename(filepath, path.extname(filepath))
if (this.options.pattern && !this.options.pattern.test(basename))
throw new Error(`Ignored ${basename} by pattern`)
if (!this.elements.has(filepath))
throw new Error(`Ignored ${basename} because isn't loaded`)
if (this.options.logger)
this.options.logger.log(
this.options.loggerPattern
? this.options.loggerPattern
.replace("$path", filepath)
.replace("$basename", basename)
.replace("$filename", filename)
: `removed ${filename}`
)
const data = this.elements.get(filepath)!
this.elements.delete(filepath)
this.md5.delete(filepath)
await this.options.onRemove?.(filepath, data)
}
}

@@ -7,4 +7,2 @@ const path = require("path")

const Inputs = jest.fn().mockReturnValueOnce("42").mockReturnValueOnce("666")
const Loaded = jest

@@ -16,4 +14,2 @@ .fn()

const Reloaded = jest.fn().mockReturnValueOnce("42").mockReturnValueOnce("666")
beforeAll(() => {

@@ -33,18 +29,23 @@ fs.writeFileSync(path.join(__dirname, "files", "b.txt"), "1")

},
onReload: async (filepath, data) => {
expect(data).toBe(Reloaded())
onChange: async (filepath, data) => {
expect(data).toBe("42")
},
onRemove: async (filepath, data) => {
expect(data).toBe("42")
},
})
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(async () => {
fs.writeFileSync(path.join(__dirname, "files", "b.txt"), "42")
await wait(150)
})
.then(async () => {
fs.unlinkSync(path.join(__dirname, "files", "b.txt"))
await wait(150)
})
.then(() => {

@@ -51,0 +52,0 @@ handler.destroy()

SocketSocket SOC 2 Logo

Product

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

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc