@ghom/handler
Advanced tools
Comparing version 2.0.0 to 3.0.0
@@ -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") | ||
}) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
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
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
14729
358
86
1
5
1
+ Addedmd5@^2.3.0
+ Addedcharenc@0.0.2(transitive)
+ Addedcrypt@0.0.2(transitive)
+ Addedis-buffer@1.1.6(transitive)
+ Addedmd5@2.3.0(transitive)