@ghom/handler
Advanced tools
Comparing version 3.0.0 to 3.1.0
@@ -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() |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
17934
15
433
0
2
85
+ Addedchokidar@^3.5.3
+ Addedanymatch@3.1.3(transitive)
+ Addedbinary-extensions@2.3.0(transitive)
+ Addedbraces@3.0.3(transitive)
+ Addedchokidar@3.6.0(transitive)
+ Addedfill-range@7.1.1(transitive)
+ Addedfsevents@2.3.3(transitive)
+ Addedglob-parent@5.1.2(transitive)
+ Addedis-binary-path@2.1.0(transitive)
+ Addedis-extglob@2.1.1(transitive)
+ Addedis-glob@4.0.3(transitive)
+ Addedis-number@7.0.0(transitive)
+ Addednormalize-path@3.0.0(transitive)
+ Addedpicomatch@2.3.1(transitive)
+ Addedreaddirp@3.6.0(transitive)
+ Addedto-regex-range@5.0.1(transitive)