@contrast/require-hook
Advanced tools
Comparing version 3.2.3 to 4.0.0
export = ExportHandlerRegistry; | ||
/** @typedef {import('./export-hook-descriptor')} ExportHookDescriptor */ | ||
/** | ||
* @template {Object} T | ||
* @typedef {import('./export-hook-descriptor')<T>} ExportHookDescriptor | ||
*/ | ||
/** | ||
* @template {Object} T | ||
* @typedef {import('./export-hook-descriptor').Handler<T>} Handler | ||
*/ | ||
/** | ||
* @template {Object} T | ||
* @typedef {Object} HandlerData | ||
* @property {Function[]} handlers | ||
* @property {Handler<T>[]} handlers | ||
* @property {PackageFinder.Metadata} metadata | ||
*/ | ||
declare class ExportHandlerRegistry { | ||
constructor({ logger }?: { | ||
logger?: Console | undefined; | ||
}); | ||
logger: Console; | ||
/** | ||
* @param {import('pino').Logger=} logger | ||
*/ | ||
constructor(logger?: import('pino').Logger | undefined); | ||
/** @type {import('pino').Logger=} */ | ||
logger: import('pino').Logger | undefined; | ||
/** | ||
* contains all modules that have been resolved and are in require cache | ||
* @type {Record<string, HandlerData>} | ||
* @type {Record<string, HandlerData<any>>} | ||
*/ | ||
resolved: Record<string, HandlerData>; | ||
/** @type {Record<string, ExportHookDescriptor[]>} */ | ||
registered: Record<string, ExportHookDescriptor[]>; | ||
/** @type {Record<string, ExportHookDescriptor[]>} */ | ||
filenameToRegistered: Record<string, ExportHookDescriptor[]>; | ||
resolved: Record<string, HandlerData<any>>; | ||
/** @type {Record<string, ExportHookDescriptor<any>[]>} */ | ||
registered: Record<string, ExportHookDescriptor<any>[]>; | ||
/** @type {Record<string, ExportHookDescriptor<any>[]>} */ | ||
filenameToRegistered: Record<string, ExportHookDescriptor<any>[]>; | ||
/** | ||
@@ -39,7 +49,8 @@ * Defined as a class method to allow easy stubbing in tests. | ||
* This gets called when an agent registers a handler for a given module. | ||
* @template {Object} T | ||
* @param {string} name | ||
* @param {ExportHookDescriptor} handlerInfo | ||
* @returns {ExportHookDescriptor[]} | ||
* @param {ExportHookDescriptor<T>} handlerInfo | ||
* @returns {ExportHookDescriptor<T>[]} | ||
*/ | ||
update(name: string, handlerInfo: ExportHookDescriptor): ExportHookDescriptor[]; | ||
update<T extends Object>(name: string, handlerInfo: import("./export-hook-descriptor")<T>): import("./export-hook-descriptor")<T>[]; | ||
/** | ||
@@ -49,10 +60,12 @@ * When an export is returned from a `require`, we want to resolve the name | ||
* metadata and handlers that have been registered for exports by that name. | ||
* @template {Object} T | ||
* @param {string} request the string passed to require() | ||
* @param {Module} parent the module executing require() | ||
* @param {boolean} isMain indicates whether the module executing require() is the entry point | ||
* @return {HandlerData=} | ||
* @return {HandlerData<T>=} | ||
*/ | ||
query(request: string, parent: Module, isMain: boolean): HandlerData | undefined; | ||
query<T_1 extends Object>(request: string, parent: Module, isMain: boolean): HandlerData<T_1> | undefined; | ||
/** | ||
* Returns handler data for a deep reference if present. | ||
* @template {Object} T | ||
* @param {string} request the string passed to require() | ||
@@ -62,15 +75,17 @@ * @param {Module} parent the module executing require() | ||
* @param {string} filename fully resolved filename of the required module | ||
* @return {ExportHookDescriptor[]=} handler data for name | ||
* @return {ExportHookDescriptor<T>[]=} handler data for name | ||
*/ | ||
findDeepReference(request: string, parent: Module, isMain: boolean, filename: string): ExportHookDescriptor[] | undefined; | ||
findDeepReference<T_2 extends Object>(request: string, parent: Module, isMain: boolean, filename: string): import("./export-hook-descriptor")<T_2>[] | undefined; | ||
/** | ||
* Returns an array of handlers that have been registered for a given module. | ||
* @template {Object} T | ||
* @param {string} request the string passed to require() | ||
* @param {string} filename fully resolved filename of the required module | ||
* @return {ExportHookDescriptor[]=} | ||
* @return {ExportHookDescriptor<T>[]=} | ||
*/ | ||
findRegisteredHandlerData(request: string, filename: string): ExportHookDescriptor[] | undefined; | ||
findRegisteredHandlerData<T_3 extends Object>(request: string, filename: string): import("./export-hook-descriptor")<T_3>[] | undefined; | ||
/** | ||
* Returns registered handlers and the module metadata for a module that has | ||
* registration for the version that is being required. | ||
* @template {Object} T | ||
* @param {Object} params | ||
@@ -80,9 +95,9 @@ * @param {string} params.request the string passed to require() | ||
* @param {PackageFinder.Metadata} params.metadata module metadata | ||
* @return {HandlerData=} | ||
* @return {HandlerData<T>=} | ||
*/ | ||
findModuleHandlerData({ request, filename, metadata }: { | ||
findModuleHandlerData<T_4 extends Object>({ request, filename, metadata }: { | ||
request: string; | ||
filename: string; | ||
metadata: PackageFinder.Metadata; | ||
}): HandlerData | undefined; | ||
}): HandlerData<T_4> | undefined; | ||
/** | ||
@@ -100,2 +115,3 @@ * Returns true if the file exists and false if it doesn't. | ||
* paths as well. | ||
* @template {Object} T | ||
* @param {Object} options | ||
@@ -105,20 +121,21 @@ * @param {string} options.request the string passed to require() | ||
* @param {Module} options.parent the module executing require() | ||
* @param {ExportHookDescriptor[]} options.registered list of registered hooks | ||
* @param {ExportHookDescriptor<T>[]} options.registered list of registered hooks | ||
*/ | ||
buildFromRegistry({ request, metadata, registered }: { | ||
buildFromRegistry<T_5 extends Object>({ request, metadata, registered }: { | ||
request: string; | ||
metadata: PackageFinder.Metadata; | ||
parent: Module; | ||
registered: ExportHookDescriptor[]; | ||
registered: import("./export-hook-descriptor")<T_5>[]; | ||
}): void; | ||
} | ||
declare namespace ExportHandlerRegistry { | ||
export { ExportHookDescriptor, HandlerData }; | ||
export { ExportHookDescriptor, Handler, HandlerData }; | ||
} | ||
type HandlerData = { | ||
handlers: Function[]; | ||
type HandlerData<T extends Object> = { | ||
handlers: Handler<T>[]; | ||
metadata: PackageFinder.Metadata; | ||
}; | ||
type ExportHookDescriptor = import('./export-hook-descriptor'); | ||
import Module = require("node/globals"); | ||
type ExportHookDescriptor<T extends Object> = import('./export-hook-descriptor')<T>; | ||
import Module = require("module"); | ||
import PackageFinder = require("./package-finder"); | ||
type Handler<T extends Object> = import('./export-hook-descriptor').Handler<T>; |
// @ts-check | ||
'use strict'; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const Module = require('module'); | ||
@@ -10,19 +11,31 @@ const semver = require('semver'); | ||
const RELATIVE_RE = /^\.\.?(?:\/|\\)/; | ||
/** @typedef {import('./export-hook-descriptor')} ExportHookDescriptor */ | ||
/** | ||
* @template {Object} T | ||
* @typedef {import('./export-hook-descriptor')<T>} ExportHookDescriptor | ||
*/ | ||
/** | ||
* @template {Object} T | ||
* @typedef {import('./export-hook-descriptor').Handler<T>} Handler | ||
*/ | ||
/** | ||
* @template {Object} T | ||
* @typedef {Object} HandlerData | ||
* @property {Function[]} handlers | ||
* @property {Handler<T>[]} handlers | ||
* @property {PackageFinder.Metadata} metadata | ||
*/ | ||
class ExportHandlerRegistry { | ||
constructor({ logger = console } = {}) { | ||
/** | ||
* @param {import('pino').Logger=} logger | ||
*/ | ||
constructor(logger) { | ||
/** @type {import('pino').Logger=} */ | ||
this.logger = logger; | ||
/** | ||
* contains all modules that have been resolved and are in require cache | ||
* @type {Record<string, HandlerData>} | ||
* @type {Record<string, HandlerData<any>>} | ||
*/ | ||
this.resolved = {}; | ||
/** @type {Record<string, ExportHookDescriptor[]>} */ | ||
/** @type {Record<string, ExportHookDescriptor<any>[]>} */ | ||
this.registered = {}; | ||
/** @type {Record<string, ExportHookDescriptor[]>} */ | ||
/** @type {Record<string, ExportHookDescriptor<any>[]>} */ | ||
this.filenameToRegistered = {}; | ||
@@ -50,3 +63,3 @@ } | ||
resolveMetadata(filename) { | ||
return PackageFinder.resolveMetadata({ logger: this.logger, filename }); | ||
return PackageFinder.resolveMetadata(filename, this.logger); | ||
} | ||
@@ -56,5 +69,6 @@ /** | ||
* This gets called when an agent registers a handler for a given module. | ||
* @template {Object} T | ||
* @param {string} name | ||
* @param {ExportHookDescriptor} handlerInfo | ||
* @returns {ExportHookDescriptor[]} | ||
* @param {ExportHookDescriptor<T>} handlerInfo | ||
* @returns {ExportHookDescriptor<T>[]} | ||
*/ | ||
@@ -74,6 +88,7 @@ update(name, handlerInfo) { | ||
* metadata and handlers that have been registered for exports by that name. | ||
* @template {Object} T | ||
* @param {string} request the string passed to require() | ||
* @param {Module} parent the module executing require() | ||
* @param {boolean} isMain indicates whether the module executing require() is the entry point | ||
* @return {HandlerData=} | ||
* @return {HandlerData<T>=} | ||
*/ | ||
@@ -133,2 +148,3 @@ query(request, parent, isMain) { | ||
* Returns handler data for a deep reference if present. | ||
* @template {Object} T | ||
* @param {string} request the string passed to require() | ||
@@ -138,3 +154,3 @@ * @param {Module} parent the module executing require() | ||
* @param {string} filename fully resolved filename of the required module | ||
* @return {ExportHookDescriptor[]=} handler data for name | ||
* @return {ExportHookDescriptor<T>[]=} handler data for name | ||
*/ | ||
@@ -167,6 +183,6 @@ findDeepReference(request, parent, isMain, filename) { | ||
err.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED') { | ||
this.logger.trace(`cannot resolve ${maybeModuleName} for ${request}`); | ||
this.logger?.trace('cannot resolve %s for %s', maybeModuleName, request); | ||
} | ||
else { | ||
this.logger.warn(err); | ||
this.logger?.warn({ err }); | ||
} | ||
@@ -187,5 +203,6 @@ } | ||
* Returns an array of handlers that have been registered for a given module. | ||
* @template {Object} T | ||
* @param {string} request the string passed to require() | ||
* @param {string} filename fully resolved filename of the required module | ||
* @return {ExportHookDescriptor[]=} | ||
* @return {ExportHookDescriptor<T>[]=} | ||
*/ | ||
@@ -198,2 +215,3 @@ findRegisteredHandlerData(request, filename) { | ||
* registration for the version that is being required. | ||
* @template {Object} T | ||
* @param {Object} params | ||
@@ -203,3 +221,3 @@ * @param {string} params.request the string passed to require() | ||
* @param {PackageFinder.Metadata} params.metadata module metadata | ||
* @return {HandlerData=} | ||
* @return {HandlerData<T>=} | ||
*/ | ||
@@ -228,2 +246,3 @@ findModuleHandlerData({ request, filename, metadata }) { | ||
* paths as well. | ||
* @template {Object} T | ||
* @param {Object} options | ||
@@ -233,7 +252,7 @@ * @param {string} options.request the string passed to require() | ||
* @param {Module} options.parent the module executing require() | ||
* @param {ExportHookDescriptor[]} options.registered list of registered hooks | ||
* @param {ExportHookDescriptor<T>[]} options.registered list of registered hooks | ||
*/ | ||
// eslint-disable-next-line complexity | ||
buildFromRegistry({ request, metadata, registered }) { | ||
/** @type {Record<string, HandlerData>} */ | ||
/** @type {Record<string, HandlerData<T>>} */ | ||
const fileHandlers = {}; | ||
@@ -246,3 +265,3 @@ // when `registered` was derived from an absolute file reference then | ||
if (!semver.satisfies(metadata.version, descriptor.version)) { | ||
this.logger.trace(`skipping hooks for semver(s) ${descriptor.version} (${request}@${metadata.version})`); | ||
this.logger?.trace('skipping handlers since package %s@%s does not satisfy semver range %s', request, metadata.version, descriptor.version); | ||
continue; | ||
@@ -252,5 +271,7 @@ } | ||
if (descriptor.file) { | ||
const absoluteFile = path.resolve(metadata.packageDir || '', descriptor.file.endsWith('.js') ? descriptor.file : `${descriptor.file}.js`); | ||
const absoluteFile = path.resolve(metadata.packageDir || '', descriptor.file.endsWith('.js') | ||
? descriptor.file | ||
: `${descriptor.file}.js`); | ||
if (!this.doesFileExist(absoluteFile)) { | ||
this.logger.trace(`Unable to resolve file ${absoluteFile}`); | ||
this.logger?.trace('unable to resolve file %s', absoluteFile); | ||
continue; | ||
@@ -270,4 +291,4 @@ } | ||
} | ||
catch (error) { | ||
this.logger.warn(error); | ||
catch (err) { | ||
this.logger?.warn({ err }); | ||
} | ||
@@ -287,15 +308,16 @@ continue; | ||
* the data will be added to the collection. | ||
* @param {Object} target The object on which to add the property | ||
* @template {Object} T | ||
* @param {Record<string, HandlerData<T>>} target The object on which to add the property | ||
* @param {string} path The string used to name the property path | ||
* @param {HandlerData} data Data to add to the collection at the path | ||
* @param {HandlerData<T>} data Data to add to the collection at the path | ||
*/ | ||
const addNonDuplicatedHandlers = (target, path, data) => { | ||
const { handlers } = data; | ||
if (!Reflect.get(target, path)) { | ||
Reflect.set(target, path, data); | ||
if (!target[path]) { | ||
target[path] = data; | ||
} | ||
else { | ||
handlers.forEach(handler => { | ||
if (Reflect.get(target, path).handlers.indexOf(handler) < 0) { | ||
Reflect.get(target, path).handlers.push(handler); | ||
handlers.forEach((handler) => { | ||
if (target[path].handlers.indexOf(handler) < 0) { | ||
target[path].handlers.push(handler); | ||
} | ||
@@ -302,0 +324,0 @@ }); |
export = ExportHookDescriptor; | ||
/** @typedef {import('./package-finder').Metadata} Metadata */ | ||
/** | ||
* @template {Object} T | ||
* @callback Handler | ||
* @param {T} mod | ||
* @param {Metadata} metadata | ||
* @returns {T} | ||
*/ | ||
/** | ||
* @template {Object} T | ||
* @typedef {Object} HookDescriptorOptions | ||
* @property {string} name | ||
* @property {string=} file | ||
* @property {Function[]=} handlers | ||
* @property {string=} version | ||
* @property {Handler<T>[]=} handlers | ||
*/ | ||
declare class ExportHookDescriptor { | ||
/** | ||
* Export information and function handlers that should be invoked on require | ||
* for a given module. | ||
* @template {Object} [T=any] | ||
*/ | ||
declare class ExportHookDescriptor<T extends Object = any> { | ||
/** | ||
* Validates the constructor params. | ||
* @param {Object} opts | ||
* @param {string} opts.name required | ||
* @param {string=} opts.name required | ||
* @param {string} opts.version must be a valid semver range | ||
@@ -18,3 +32,3 @@ * @throws {Error} | ||
static validate({ name, version }: { | ||
name: string; | ||
name?: string | undefined; | ||
version: string; | ||
@@ -24,29 +38,31 @@ }): void; | ||
* A static factory function for creating descriptors with different params. | ||
* @param {HookDescriptorOptions|string} descriptor export info and handlers | ||
* @returns {ExportHookDescriptor} | ||
* @template {Object} T | ||
* @param {HookDescriptorOptions<T> | string} descriptor export info and handlers | ||
* @returns {ExportHookDescriptor<T>} | ||
*/ | ||
static create(descriptor: HookDescriptorOptions | string): ExportHookDescriptor; | ||
static create<T_1 extends Object>(descriptor: string | HookDescriptorOptions<T_1>): import("./export-hook-descriptor")<T_1>; | ||
/** | ||
* Export information and function handlers that should be invoked on require. | ||
* @param {HookDescriptorOptions=} options | ||
* @param {HookDescriptorOptions<T>} options | ||
*/ | ||
constructor({ name, file, handlers, version }?: HookDescriptorOptions | undefined); | ||
constructor({ name, file, version, handlers, }?: HookDescriptorOptions<T>); | ||
/** @type {string} */ | ||
file: string; | ||
/** @type {string} */ | ||
name: string; | ||
/** @type {string=} */ | ||
file: string | undefined; | ||
/** @type {string} */ | ||
version: string; | ||
/** @type {Function[]} */ | ||
handlers: Function[]; | ||
/** @type {Handler<T>[]} */ | ||
handlers: Handler<T>[]; | ||
} | ||
declare namespace ExportHookDescriptor { | ||
export { DEFAULT_VERSION, HookDescriptorOptions }; | ||
export { DEFAULT_VERSION, Metadata, Handler, HookDescriptorOptions }; | ||
} | ||
type HookDescriptorOptions = { | ||
type Handler<T extends Object> = (mod: T, metadata: Metadata) => T; | ||
type HookDescriptorOptions<T extends Object> = { | ||
name: string; | ||
file?: string | undefined; | ||
handlers?: Function[] | undefined; | ||
version?: string | undefined; | ||
handlers?: Handler<T>[] | undefined; | ||
}; | ||
declare const DEFAULT_VERSION: ">=0.0.0"; | ||
declare const DEFAULT_VERSION: "*"; | ||
type Metadata = import('./package-finder').Metadata; |
// @ts-check | ||
'use strict'; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const Semver = require('semver'); | ||
const DEFAULT_VERSION = '>=0.0.0'; | ||
const DEFAULT_VERSION = '*'; | ||
/** @typedef {import('./package-finder').Metadata} Metadata */ | ||
/** | ||
* @template {Object} T | ||
* @callback Handler | ||
* @param {T} mod | ||
* @param {Metadata} metadata | ||
* @returns {T} | ||
*/ | ||
/** | ||
* @template {Object} T | ||
* @typedef {Object} HookDescriptorOptions | ||
* @property {string} name | ||
* @property {string=} file | ||
* @property {Function[]=} handlers | ||
* @property {string=} version | ||
* @property {Handler<T>[]=} handlers | ||
*/ | ||
/** | ||
* Export information and function handlers that should be invoked on require | ||
* for a given module. | ||
* @template {Object} [T=any] | ||
*/ | ||
class ExportHookDescriptor { | ||
/** | ||
* Export information and function handlers that should be invoked on require. | ||
* @param {HookDescriptorOptions=} options | ||
* @param {HookDescriptorOptions<T>} options | ||
*/ | ||
// @ts-ignore this could use a refactor | ||
constructor({ | ||
// @ts-ignore we wouldn't need to validate these if we were actually using TypeScript | ||
name = null, | ||
// @ts-ignore we wouldn't need to validate these if we were actually using TypeScript | ||
file = null, handlers = [], version = DEFAULT_VERSION } = {}) { | ||
constructor({ name, file, version = DEFAULT_VERSION, handlers = [], } = {}) { | ||
ExportHookDescriptor.validate({ name, version }); | ||
/** @type {string} */ | ||
this.name = name; | ||
/** @type {string=} */ | ||
this.file = file; | ||
/** @type {string} */ | ||
this.name = name; | ||
/** @type {string} */ | ||
this.version = version; | ||
/** @type {Function[]} */ | ||
/** @type {Handler<T>[]} */ | ||
this.handlers = handlers; | ||
@@ -36,3 +46,3 @@ } | ||
* @param {Object} opts | ||
* @param {string} opts.name required | ||
* @param {string=} opts.name required | ||
* @param {string} opts.version must be a valid semver range | ||
@@ -46,3 +56,3 @@ * @throws {Error} | ||
if (!Semver.validRange(version)) { | ||
throw new Error(`Invalid version: \`${version || 'undefined'}\``); | ||
throw new Error(`Invalid version range provided: "${version}"`); | ||
} | ||
@@ -52,4 +62,5 @@ } | ||
* A static factory function for creating descriptors with different params. | ||
* @param {HookDescriptorOptions|string} descriptor export info and handlers | ||
* @returns {ExportHookDescriptor} | ||
* @template {Object} T | ||
* @param {HookDescriptorOptions<T> | string} descriptor export info and handlers | ||
* @returns {ExportHookDescriptor<T>} | ||
*/ | ||
@@ -56,0 +67,0 @@ static create(descriptor) { |
export = HandlerInvoker; | ||
/** | ||
* @template {Object} T | ||
* @typedef {import('./export-hook-descriptor').Handler<T>} Handler | ||
*/ | ||
/** @typedef {import('./package-finder').Metadata} Metadata */ | ||
declare class HandlerInvoker { | ||
/** | ||
* @param {Object} options | ||
* @param {any=} options.logger | ||
* @param {WeakMap<Object, WeakSet<Function>>} seen Maps exports to a set of run handlers | ||
* @param {import('pino').Logger=} logger | ||
*/ | ||
constructor({ logger }?: { | ||
logger?: any | undefined; | ||
}, seen?: WeakMap<Object, WeakSet<Function>>); | ||
logger: any; | ||
/** @type {WeakMap<Object, WeakSet<Function>>} */ | ||
seen: WeakMap<Object, WeakSet<Function>>; | ||
constructor(logger?: import('pino').Logger | undefined); | ||
/** @type {import('pino').Logger=} */ | ||
logger: import('pino').Logger | undefined; | ||
/** @type {WeakMap<Object, WeakSet<Handler<any>>>} */ | ||
seen: WeakMap<Object, WeakSet<Handler<any>>>; | ||
/** | ||
* Filters a collection of handlers to those which have not run for the | ||
* provided export. | ||
* @template T | ||
* @param {Function[]} handlers Collection of handlers | ||
* @template {Object} T | ||
* @param {T} xport The exported module | ||
* @returns {Function[]} | ||
* @param {Handler<T>[]} handlers Collection of handlers | ||
* @returns {Handler<T>[]} | ||
*/ | ||
filter<T>(xport: T, handlers: Function[]): Function[]; | ||
filter<T extends Object>(xport: T, handlers: Handler<T>[]): Handler<T>[]; | ||
/** | ||
* Invoke all handlers in the provided collection that have not yet been run | ||
* for the provided export value. | ||
* @template T | ||
* @template {Object} T | ||
* @param {T} xport The exported module | ||
* @param {Function[]} handlers The handlers to invoke | ||
* @param {Object} metadata Export metadata to pass to the handlers | ||
* @param {Handler<T>[]} handlers The handlers to invoke | ||
* @param {Metadata} metadata Export metadata to pass to the handlers | ||
* @returns {T} | ||
*/ | ||
invoke<T_1>(xport: T_1, handlers: Function[], metadata: Object): T_1; | ||
invoke<T_1 extends Object>(xport: T_1, handlers: Handler<T_1>[], metadata: Metadata): T_1; | ||
/** | ||
* @template T | ||
* @template {Object} T | ||
* @param {T} xport The exported module | ||
*/ | ||
reset<T_2>(xport: T_2): void; | ||
reset<T_2 extends Object>(xport: T_2): void; | ||
} | ||
declare namespace HandlerInvoker { | ||
export { Handler, Metadata }; | ||
} | ||
type Handler<T extends Object> = import('./export-hook-descriptor').Handler<T>; | ||
type Metadata = import('./package-finder').Metadata; |
// @ts-check | ||
'use strict'; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
/** | ||
* @template {Object} T | ||
* @typedef {import('./export-hook-descriptor').Handler<T>} Handler | ||
*/ | ||
/** @typedef {import('./package-finder').Metadata} Metadata */ | ||
class HandlerInvoker { | ||
/** | ||
* @param {Object} options | ||
* @param {any=} options.logger | ||
* @param {WeakMap<Object, WeakSet<Function>>} seen Maps exports to a set of run handlers | ||
* @param {import('pino').Logger=} logger | ||
*/ | ||
constructor({ logger = console } = {}, seen = new WeakMap()) { | ||
constructor(logger) { | ||
/** @type {import('pino').Logger=} */ | ||
this.logger = logger; | ||
/** @type {WeakMap<Object, WeakSet<Function>>} */ | ||
this.seen = seen; | ||
/** @type {WeakMap<Object, WeakSet<Handler<any>>>} */ | ||
this.seen = new WeakMap(); | ||
} | ||
@@ -17,6 +22,6 @@ /** | ||
* provided export. | ||
* @template T | ||
* @param {Function[]} handlers Collection of handlers | ||
* @template {Object} T | ||
* @param {T} xport The exported module | ||
* @returns {Function[]} | ||
* @param {Handler<T>[]} handlers Collection of handlers | ||
* @returns {Handler<T>[]} | ||
*/ | ||
@@ -46,6 +51,6 @@ filter(xport, handlers) { | ||
* for the provided export value. | ||
* @template T | ||
* @template {Object} T | ||
* @param {T} xport The exported module | ||
* @param {Function[]} handlers The handlers to invoke | ||
* @param {Object} metadata Export metadata to pass to the handlers | ||
* @param {Handler<T>[]} handlers The handlers to invoke | ||
* @param {Metadata} metadata Export metadata to pass to the handlers | ||
* @returns {T} | ||
@@ -57,9 +62,7 @@ */ | ||
try { | ||
this.logger?.trace({ metadata }, 'invoking handler: %s', metadata.name); | ||
return handler(acc, metadata) || acc; | ||
} | ||
catch (err) { | ||
this.logger.error('require-hook handler failed: %s', { | ||
...metadata, | ||
err | ||
}); | ||
this.logger?.error({ err, metadata }, 'error invoking handler: %s', metadata.name); | ||
return acc; | ||
@@ -75,3 +78,3 @@ } | ||
/** | ||
* @template T | ||
* @template {Object} T | ||
* @param {T} xport The exported module | ||
@@ -78,0 +81,0 @@ */ |
// @ts-check | ||
'use strict'; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const path = require('path'); | ||
@@ -4,0 +5,0 @@ const pathSepRegex = new RegExp(`\\${path.sep}`, 'g'); |
export = RequireHook; | ||
import RequireHook = require("./require-hook"); | ||
/** | ||
* @template {Object} T | ||
* @typedef {import('./export-hook-descriptor').Handler<T>} Handler | ||
*/ | ||
/** | ||
* @typedef {Object} Descriptor | ||
* @property {string=} file if provided, the file under the module's root that we want to hook. otherwise, the module's `main` will be hooked. | ||
* @property {string} name module name, as passed to `require`, to handle. | ||
* @property {string=} version if provided, hooks will only execute against an installed module that matches the SemVer version range | ||
* @property {string=} module alternative to `name`, taking precedent when provided. | ||
*/ | ||
/** | ||
* Allows clients to register function handlers which run as a 'post-hook' at | ||
* require-time. | ||
*/ | ||
declare class RequireHook { | ||
/** | ||
* Coerces a string into a minimal object that can be made into a descriptor. | ||
* @param {Descriptor | string} descriptor The export descriptor | ||
* @returns {Descriptor} | ||
*/ | ||
static normalizeDescriptor(descriptor: Descriptor | string): Descriptor; | ||
/** | ||
* @param {import('pino').Logger=} logger | ||
*/ | ||
constructor(logger?: import('pino').Logger | undefined); | ||
/** @type {import('pino').Logger=} */ | ||
logger: import('pino').Logger | undefined; | ||
originalLoad: any; | ||
/** @type {HandlerInvoker} */ | ||
invoker: HandlerInvoker; | ||
/** @type {ExportHandlerRegistry} */ | ||
registry: ExportHandlerRegistry; | ||
/** @type {WeakMap<Object, Object>} */ | ||
requiredModules: WeakMap<Object, Object>; | ||
/** @type {Set<string>} */ | ||
resets: Set<string>; | ||
/** | ||
* Registers handlers to run afer the described module is required. | ||
* @template {Object} T | ||
* @param {Descriptor | string} descriptor describes the module to hook | ||
* @param {Handler<T>[]} handlers the function hooks to execute after require | ||
*/ | ||
resolve<T extends Object>(descriptor: Descriptor | string, ...handlers: Handler<T>[]): void; | ||
/** | ||
* Provided with an export, a collection of handlers, and metadata, will | ||
* invoke only the handlers which have not yet run on the export instance. | ||
* @template {Object} T | ||
* @param {T} xport the exported value of a required module | ||
* @param {Handler<T>[]} handlers the function hooks to execute on require | ||
* @param {import('./package-finder').Metadata} metadata the export's metadata | ||
* @returns {T} | ||
*/ | ||
runRequireHandlers<T_1 extends Object>(xport: T_1, handlers: Handler<T_1>[], metadata: import('./package-finder').Metadata): T_1; | ||
/** | ||
* Overrides the Module._load method to run registered handlers _after_ | ||
* the modules have loaded. This method is invoked by require. | ||
*/ | ||
install(): void; | ||
/** | ||
* Checks if module name exists in resets set. If so, it will remove it from | ||
* the set as well as remove it from the invoker WeakMap. This will force | ||
* instrumentation handlers to re-run. This use case is only used for testing | ||
* of the node agent in certain cases. | ||
* @template {Object} T | ||
* @param {string} request the string passed to require() | ||
* @param {T} xport the exported value of a required module | ||
*/ | ||
maybeClearHandlers<T_2 extends Object>(request: string, xport: T_2): void; | ||
/** | ||
* Resets Module's _load method to the original value. | ||
*/ | ||
uninstall(): void; | ||
/** | ||
* Resets the seen handlers for a given module - they will be re-run on next | ||
* require. | ||
* @param {string} request the string passed to require() | ||
*/ | ||
reset(request: string): void; | ||
} | ||
declare namespace RequireHook { | ||
export { ExportHandlerRegistry, ExportHookDescriptor, HandlerInvoker, PackageFinder, RequireHook, Handler, Descriptor }; | ||
} | ||
import HandlerInvoker = require("./handler-invoker"); | ||
import ExportHandlerRegistry = require("./export-handler-registry"); | ||
type Descriptor = { | ||
/** | ||
* if provided, the file under the module's root that we want to hook. otherwise, the module's `main` will be hooked. | ||
*/ | ||
file?: string | undefined; | ||
/** | ||
* module name, as passed to `require`, to handle. | ||
*/ | ||
name: string; | ||
/** | ||
* if provided, hooks will only execute against an installed module that matches the SemVer version range | ||
*/ | ||
version?: string | undefined; | ||
/** | ||
* alternative to `name`, taking precedent when provided. | ||
*/ | ||
module?: string | undefined; | ||
}; | ||
type Handler<T extends Object> = import('./export-hook-descriptor').Handler<T>; | ||
import ExportHookDescriptor = require("./export-hook-descriptor"); | ||
import PackageFinder = require("./package-finder"); |
135
lib/index.js
@@ -0,2 +1,5 @@ | ||
// @ts-check | ||
'use strict'; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const Module = require('module'); | ||
const ExportHandlerRegistry = require('./export-handler-registry'); | ||
@@ -6,3 +9,133 @@ const ExportHookDescriptor = require('./export-hook-descriptor'); | ||
const PackageFinder = require('./package-finder'); | ||
const RequireHook = require('./require-hook'); | ||
/** | ||
* @template {Object} T | ||
* @typedef {import('./export-hook-descriptor').Handler<T>} Handler | ||
*/ | ||
/** | ||
* @typedef {Object} Descriptor | ||
* @property {string=} file if provided, the file under the module's root that we want to hook. otherwise, the module's `main` will be hooked. | ||
* @property {string} name module name, as passed to `require`, to handle. | ||
* @property {string=} version if provided, hooks will only execute against an installed module that matches the SemVer version range | ||
* @property {string=} module alternative to `name`, taking precedent when provided. | ||
*/ | ||
/** | ||
* Allows clients to register function handlers which run as a 'post-hook' at | ||
* require-time. | ||
*/ | ||
class RequireHook { | ||
/** | ||
* @param {import('pino').Logger=} logger | ||
*/ | ||
constructor(logger) { | ||
/** @type {import('pino').Logger=} */ | ||
this.logger = logger; | ||
// set this up before we start patching Module methods | ||
this.originalLoad = Reflect.get(Module, '_load'); | ||
/** @type {HandlerInvoker} */ | ||
this.invoker = new HandlerInvoker(logger); | ||
/** @type {ExportHandlerRegistry} */ | ||
this.registry = new ExportHandlerRegistry(logger); | ||
/** @type {WeakMap<Object, Object>} */ | ||
this.requiredModules = new WeakMap(); | ||
/** @type {Set<string>} */ | ||
this.resets = new Set(); | ||
} | ||
/** | ||
* Registers handlers to run afer the described module is required. | ||
* @template {Object} T | ||
* @param {Descriptor | string} descriptor describes the module to hook | ||
* @param {Handler<T>[]} handlers the function hooks to execute after require | ||
*/ | ||
resolve(descriptor, ...handlers) { | ||
const { file, name, version } = RequireHook.normalizeDescriptor(descriptor); | ||
const info = ExportHookDescriptor.create({ file, handlers, name, version }); | ||
this.registry.update(name, info); | ||
} | ||
/** | ||
* Provided with an export, a collection of handlers, and metadata, will | ||
* invoke only the handlers which have not yet run on the export instance. | ||
* @template {Object} T | ||
* @param {T} xport the exported value of a required module | ||
* @param {Handler<T>[]} handlers the function hooks to execute on require | ||
* @param {import('./package-finder').Metadata} metadata the export's metadata | ||
* @returns {T} | ||
*/ | ||
runRequireHandlers(xport, handlers, metadata) { | ||
return this.invoker.invoke(xport, handlers, metadata); | ||
} | ||
/** | ||
* Overrides the Module._load method to run registered handlers _after_ | ||
* the modules have loaded. This method is invoked by require. | ||
*/ | ||
install() { | ||
this.logger?.trace('Applying Module._load override'); | ||
const self = this; | ||
/** | ||
* @this {Module} | ||
* @param {string} request the string passed to require() | ||
* @param {Module} parent the module executing require() | ||
* @param {boolean} isMain indicates whether the module executing require() is the entry point | ||
*/ | ||
const __loadOverride = function __loadOverride(request, parent, isMain) { | ||
let xportSubstitution; | ||
const exportHandlerInfo = self.registry.query(request, parent, isMain); | ||
const xport = Reflect.apply(self.originalLoad, this, [ | ||
request, | ||
parent, | ||
isMain, | ||
]); | ||
if (exportHandlerInfo) { | ||
self.maybeClearHandlers(request, xport); | ||
xportSubstitution = self.runRequireHandlers(self.requiredModules.get(xport) || xport, exportHandlerInfo.handlers, exportHandlerInfo.metadata); | ||
self.requiredModules.set(xport, xportSubstitution); | ||
} | ||
return self.requiredModules.get(xport) || xport; | ||
}; | ||
Reflect.set(Module, '_load', __loadOverride); | ||
} | ||
/** | ||
* Checks if module name exists in resets set. If so, it will remove it from | ||
* the set as well as remove it from the invoker WeakMap. This will force | ||
* instrumentation handlers to re-run. This use case is only used for testing | ||
* of the node agent in certain cases. | ||
* @template {Object} T | ||
* @param {string} request the string passed to require() | ||
* @param {T} xport the exported value of a required module | ||
*/ | ||
maybeClearHandlers(request, xport) { | ||
if (this.resets.has(request)) { | ||
this.resets.delete(request); | ||
this.invoker.reset(xport); | ||
} | ||
} | ||
/** | ||
* Resets Module's _load method to the original value. | ||
*/ | ||
uninstall() { | ||
this.logger?.trace('Removing Module._load override'); | ||
Reflect.set(Module, '_load', this.originalLoad); | ||
} | ||
/** | ||
* Resets the seen handlers for a given module - they will be re-run on next | ||
* require. | ||
* @param {string} request the string passed to require() | ||
*/ | ||
reset(request) { | ||
this.resets.add(request); | ||
} | ||
/** | ||
* Coerces a string into a minimal object that can be made into a descriptor. | ||
* @param {Descriptor | string} descriptor The export descriptor | ||
* @returns {Descriptor} | ||
*/ | ||
static normalizeDescriptor(descriptor) { | ||
if (typeof descriptor === 'string') { | ||
return { name: descriptor }; | ||
} | ||
if (descriptor.module) { | ||
descriptor.name = descriptor.module; | ||
} | ||
return descriptor; | ||
} | ||
} | ||
module.exports = RequireHook; | ||
@@ -9,0 +142,0 @@ module.exports.ExportHandlerRegistry = ExportHandlerRegistry; |
@@ -1,32 +0,3 @@ | ||
export = PackageFinder; | ||
/** | ||
* @typedef {Object} Metadata | ||
* @property {string} name the name of the module being required as indicated by its package.json | ||
* @property {string} version the version of the module being required as indicated by its package.json | ||
* @property {string} packageDir the location of the module being required | ||
* @property {string=} main `main` field from the package being required's package.json | ||
*/ | ||
declare class PackageFinder { | ||
export type Metadata = { | ||
/** | ||
* Resolves the metadata of a package given it's resolved name. | ||
* @param {Object} params | ||
* @param {any=} params.logger | ||
* @param {string=} params.filename Absolute path to the module file | ||
* @returns {Metadata?} | ||
*/ | ||
static resolveMetadata({ logger, filename }?: { | ||
logger?: any | undefined; | ||
filename?: string | undefined; | ||
}): Metadata | null; | ||
/** | ||
* @param {string} name | ||
* @returns {boolean} | ||
*/ | ||
static isNative(name: string): boolean; | ||
} | ||
declare namespace PackageFinder { | ||
export { Metadata }; | ||
} | ||
type Metadata = { | ||
/** | ||
* the name of the module being required as indicated by its package.json | ||
@@ -47,2 +18,23 @@ */ | ||
main?: string | undefined; | ||
file: string; | ||
}; | ||
/** | ||
* @typedef {Object} Metadata | ||
* @property {string} name the name of the module being required as indicated by its package.json | ||
* @property {string} version the version of the module being required as indicated by its package.json | ||
* @property {string} packageDir the location of the module being required | ||
* @property {string=} main `main` field from the package being required's package.json | ||
* @property {string} file | ||
*/ | ||
/** | ||
* @param {string} name | ||
* @returns {boolean} | ||
*/ | ||
export function isNative(name: string): boolean; | ||
/** | ||
* Resolves the metadata of a package given it's resolved name. | ||
* @param {string=} filename Absolute path to the module file | ||
* @param {import('pino').Logger=} logger | ||
* @returns {Metadata?} | ||
*/ | ||
export function resolveMetadata(filename?: string | undefined, logger?: import('pino').Logger | undefined): Metadata | null; |
// @ts-check | ||
'use strict'; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const Module = require('module'); | ||
@@ -12,50 +13,53 @@ const parentPackageJson = require('parent-package-json'); | ||
* @property {string=} main `main` field from the package being required's package.json | ||
* @property {string} file | ||
*/ | ||
class PackageFinder { | ||
/** | ||
* Resolves the metadata of a package given it's resolved name. | ||
* @param {Object} params | ||
* @param {any=} params.logger | ||
* @param {string=} params.filename Absolute path to the module file | ||
* @returns {Metadata?} | ||
*/ | ||
static resolveMetadata({ logger = console, filename } = {}) { | ||
if (!filename) { | ||
return null; | ||
} | ||
if (PackageFinder.isNative(filename)) { | ||
return { | ||
name: filename, | ||
version: process.version.substring(1), | ||
packageDir: `node:${filename}`, | ||
}; | ||
} | ||
const manifest = parentPackageJson(filename); | ||
if (!manifest) { | ||
logger.error('Unable to find package.json for %s', filename); | ||
return null; | ||
} | ||
try { | ||
const { name, version, main } = manifest.parse(); | ||
return { | ||
name, | ||
version, | ||
packageDir: path.dirname(manifest.path), | ||
main, | ||
}; | ||
} | ||
catch (err) { | ||
logger.error('Unable to parse %s, err: %o', manifest.path, err); | ||
return null; | ||
} | ||
/** | ||
* @param {string} name | ||
* @returns {boolean} | ||
*/ | ||
function isNative(name) { | ||
return Module.builtinModules.indexOf(name) >= 0; | ||
} | ||
/** | ||
* Resolves the metadata of a package given it's resolved name. | ||
* @param {string=} filename Absolute path to the module file | ||
* @param {import('pino').Logger=} logger | ||
* @returns {Metadata?} | ||
*/ | ||
function resolveMetadata(filename, logger) { | ||
if (!filename) { | ||
return null; | ||
} | ||
/** | ||
* @param {string} name | ||
* @returns {boolean} | ||
*/ | ||
static isNative(name) { | ||
return Module.builtinModules.indexOf(name) >= 0; | ||
if (isNative(filename)) { | ||
return { | ||
name: filename, | ||
version: process.version.substring(1), | ||
packageDir: `node:${filename}`, | ||
file: `node:${filename}`, | ||
}; | ||
} | ||
const manifest = parentPackageJson(filename); | ||
if (!manifest) { | ||
logger?.error('unable to find package.json for %s', filename); | ||
return null; | ||
} | ||
try { | ||
const { name, version, main } = manifest.parse(); | ||
return { | ||
name, | ||
version, | ||
packageDir: path.dirname(manifest.path), | ||
main, | ||
file: filename, | ||
}; | ||
} | ||
catch (err) { | ||
logger?.error({ err }, 'unable to parse %s', manifest.path); | ||
return null; | ||
} | ||
} | ||
module.exports = PackageFinder; | ||
module.exports = { | ||
isNative, | ||
resolveMetadata, | ||
}; | ||
//# sourceMappingURL=package-finder.js.map |
{ | ||
"name": "@contrast/require-hook", | ||
"version": "3.2.3", | ||
"version": "4.0.0", | ||
"description": "Post hooks for Module.prototype.require", | ||
@@ -12,3 +12,3 @@ "license": "MIT", | ||
"engines": { | ||
"node": ">=12.13.0" | ||
"node": ">=14.15.0" | ||
}, | ||
@@ -29,21 +29,22 @@ "scripts": { | ||
"parent-package-json": "^2.0.1", | ||
"semver": "^7.5.3" | ||
"semver": "^7.5.4" | ||
}, | ||
"devDependencies": { | ||
"@contrast/eslint-config": "^3.0.1", | ||
"@ls-lint/ls-lint": "^1.11.0", | ||
"@tsconfig/node12": "^1.0.9", | ||
"@types/node": "^12.20.47", | ||
"@ls-lint/ls-lint": "^2.1.0", | ||
"@tsconfig/node14": "^14.1.0", | ||
"@types/mocha": "^10.0.1", | ||
"@types/node": "^14.18.58", | ||
"@types/parent-package-json": "^2.0.2", | ||
"@types/semver": "^7.3.9", | ||
"chai": "^4.3.6", | ||
"codecov": "^3.8.3", | ||
"husky": "^7.0.4", | ||
"lint-staged": "^13.2.1", | ||
"mocha": "^9.2.2", | ||
"@types/semver": "^7.5.1", | ||
"chai": "^4.3.8", | ||
"husky": "^8.0.3", | ||
"lint-staged": "^14.0.1", | ||
"mocha": "^10.2.0", | ||
"nyc": "^15.1.0", | ||
"sinon": "^13.0.1", | ||
"pino": "^8.15.0", | ||
"sinon": "^15.2.0", | ||
"sinon-chai": "^3.7.0", | ||
"typescript": "^4.6.3" | ||
"typescript": "^5.2.2" | ||
} | ||
} |
# @contrast/require-hook | ||
[![codecov](https://codecov.io/gh/Contrast-Security-Inc/node-require-hook/branch/main/graph/badge.svg?token=9WOHKBL4LS)](https://codecov.io/gh/Contrast-Security-Inc/node-require-hook) | ||
![Pipeline Status](https://github.com/Contrast-Security-Inc/node-require-hook/workflows/Unit%20Tests%20and%20Build/badge.svg) | ||
@@ -7,7 +7,4 @@ | ||
## Module Flow | ||
![require-hook flow](module-flow.svg) | ||
## Usage | ||
## API | ||
### Class: `RequireHook` | ||
@@ -22,2 +19,5 @@ | ||
The `RequireHook` constructor accepts a [`pino`](https://github.com/pinojs/pino) | ||
logger as an argument. | ||
#### `.resolve(descriptor, ...handlers)` | ||
@@ -27,28 +27,32 @@ | ||
- `descriptor`: This can be a string or an object describing the module you want to intercept. | ||
If a string is used, or if the version field of the descriptor isn't set, all versions of the | ||
described module will be matched. Descriptors can have a `name`, `version`, and `file` property. | ||
- `descriptor`: This can be a string or an object describing the module you want | ||
to intercept. If a string is used, or if the version field of the descriptor | ||
isn't set, all versions of the described module will be matched. Descriptors | ||
can have a `name`, `version`, and `file` property. | ||
- `handlers`: The remaning arguments are the handlers which will be invoked when the described | ||
module is `require`'d. Each handler is passed the exported module and metadata including the | ||
module's root directory and its name and version as seen in its `package.json` file. If a | ||
handler returns a truthy value, then that value will replace the return value of `require`. | ||
- `handlers`: The remaning arguments are the handlers which will be invoked when | ||
the described module is `require`'d. Each handler is passed the exported | ||
module and metadata including the module's root directory and its name and | ||
version as seen in its `package.json` file. If a handler returns a truthy | ||
value, then that value will replace the return value of `require`. | ||
_**Note:**_ Registered handlers run _once_ per unique instance of an export matching a descriptor. | ||
_**Note:**_ Registered handlers run _once_ per unique instance of an export | ||
matching a descriptor. | ||
#### `.install()` | ||
This will monkey-patch `Module.prototype.require` so that exports can be intercepted. The | ||
monkey-patching will only happen once regardless of how many times this is invoked. | ||
This will monkey-patch `Module.prototype.require` so that exports can be | ||
intercepted. The monkey-patching will only happen once regardless of how many | ||
times this is invoked. | ||
#### `.uninstall()` | ||
This will reset `Module.prototype.require` to its value before being monkey-patched by the instance. | ||
This will reset `Module.prototype.require` to its value before being | ||
monkey-patched by the instance. | ||
## Examples | ||
**Use case:** For `express` versions greater than or equal to 4, intercept the export of the | ||
package's `lib/view.js` file (relative to the package's base directory) and apply a tag to the | ||
exported function. | ||
**Use case:** For `express` versions greater than or equal to 4, intercept the | ||
export of the package's `lib/view.js` file (relative to the package's base | ||
directory) and apply a tag to the exported function. | ||
@@ -58,11 +62,12 @@ ```javascript | ||
const requireHook = new RequireHook(); | ||
requireHook.resolve({ | ||
requireHook.resolve( | ||
{ | ||
name: 'express', | ||
version: '>=4', | ||
file: 'lib/view.js' | ||
file: 'lib/view.js', | ||
}, | ||
(xport, metadata) => { | ||
(xport, metadata) => { | ||
// Read from the package.json: | ||
// - metadata.name | ||
// - metadata.version | ||
// - metadata.name | ||
// - metadata.version | ||
// Absolute path to file: | ||
@@ -73,7 +78,8 @@ // - metadata.packageDir | ||
xport['I was intercepted'] = true; | ||
} | ||
}, | ||
); | ||
``` | ||
**Use case:** Intercept all versions of `body-parser` and replace the exported functions. | ||
**Use case:** Intercept all versions of `body-parser` and replace the exported | ||
functions. | ||
@@ -84,14 +90,13 @@ ```javascript | ||
requireHook.resolve({ name: 'body-parser' }, (xport, metadata) => { | ||
// Read from the package.json: | ||
// - metadata.name | ||
// - metadata.version | ||
// Absolute path to file: | ||
// - metadata.packageDir | ||
// Read from the package.json: | ||
// - metadata.name | ||
// - metadata.version | ||
// Absolute path to file: | ||
// - metadata.packageDir | ||
// xport === function bodyParser() { /*...*/ } | ||
return function bodyParserReplacement() { | ||
/*...*/ | ||
} | ||
} | ||
); | ||
// xport === function bodyParser() { /*...*/ } | ||
return function bodyParserReplacement() { | ||
/*...*/ | ||
}; | ||
}); | ||
``` |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
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
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
62397
1110
97
16
20
Updatedsemver@^7.5.4