Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add types and fix minor bug #16

Merged
merged 2 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion .xo-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,24 @@ module.exports = {
'n/prefer-global/process': 'off',
'prefer-object-spread': 'off',
'unicorn/prefer-includes': 'off'
}
},
overrides: [
{
files: ['**/*.d.ts'],
rules: {
'no-unused-vars': 'off',
'@typescript-eslint/naming-convention': 'off',
'no-redeclare': 'off',
'@typescript-eslint/no-redeclare': 'off'
}
},
{
files: ['**/*.test-d.ts'],
rules: {
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-confusing-void-expression': 'off', // Conflicts with `expectError` assertion.
'@typescript-eslint/no-unsafe-assignment': 'off'
}
}
]
};
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"signale": "^1.4.0",
"sinon": "^15.2.0",
"tinyify": "3.0.0",
"tsd": "^0.31.0",
"xo": "^0.56.0"
},
"engines": {
Expand Down Expand Up @@ -120,7 +121,7 @@
"build": "npm run build:clean && npm run build:lib && npm run build:dist",
"build:clean": "rimraf lib dist",
"build:dist": "npm run browserify && npm run minify",
"build:lib": "babel --config-file ./.lib.babelrc.json src --out-dir lib",
"build:lib": "babel --config-file ./.lib.babelrc.json src --out-dir lib --copy-files",
"lint": "xo --fix && remark . -qfo && fixpack",
"lint-build": "npm run lint-lib && npm run lint-dist",
"lint-dist": "eslint --no-inline-config -c .dist.eslintrc.json dist",
Expand All @@ -129,7 +130,8 @@
"nyc": "cross-env NODE_ENV=test nyc ava",
"prepare": "husky install",
"pretest": "npm run lint",
"test": "npm run build && npm run lint-build && npm run nyc"
"test": "npm run build && npm run lint-build && tsd && npm run nyc"
},
"types": "lib/index.d.ts",
"unpkg": "dist/axe.min.js"
}
247 changes: 247 additions & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
export default Axe;

declare const Axe: Axe.Constructor;

declare namespace Axe {
// These should match the defaults in the index file
namespace Defaults {
type OmittedLoggerKeys = 'config' | 'log';
type Levels = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
}

export type Constructor = new <TLogger extends Logger = Console>(
config?: Options<TLogger>
) => Axe<TLogger>;

export type Axe<TLogger extends Logger = Console> = ParentLogger<TLogger> &
Prototype &
LoggerMethods &
LoggerMethodAliases;

// This is the inherited methods from the logger object
export type ParentLogger<T extends Logger> = Omit<
{
[K in Exclude<keyof T, Defaults.OmittedLoggerKeys>]: T[K];
},
Defaults.Levels
>;

export type LoggerMethod = (...args: any[]) => Promise<void>;

export type LoggerMethods = {
[K in Defaults.Levels]: LoggerMethod;
};

export type LoggerMethodAliases = {
err: LoggerMethods['error'];
warning: LoggerMethods['warn'];
};

export type Prototype = {
log: (...args: any[]) => Promise<void>;

setLevel(leveL: string): void;
getNormalizedLevel(level: string): string;
setName(name: string): void;
pre(level: string, fn: PreHook): void;
post(level: string, fn: PostHook): void;
};

type Logger<
ObjectType = {
info?: (...args: any[]) => any;
log?: (...args: any[]) => any;
},
KeysType extends keyof ObjectType = keyof ObjectType
> =
// Require at least one of the keys
{
[Key in KeysType]-?: Required<Pick<ObjectType, Key>> &
Partial<Pick<ObjectType, Exclude<KeysType, Key>>>;
}[KeysType] &
Record<string, any>;

/**
* A pre-hook is a function that runs before the logger method is invoked.
* It receives the method name, error, message, and metadata as arguments.
* It should return an array of the error, message, and metadata.
*
* @param method - The method name that will be invoked (e.g. 'info', 'warn', 'error', 'fatal')
* @param err - The error object (if any)
* @param message - The message to log (if any)
* @param meta - The metadata object to log (if any)
*
* @returns An array of the error, message, and metadata
*/
export type PreHook = (
method: string,
err: any,
message: any,
meta: any
) => [any, any, any];

/**
* A post-hook is a function that runs after the logger method is invoked.
*
* @param method - The method name that was invoked (e.g. 'info', 'warn', 'error', 'fatal')
* @param err - The error object (if any)
* @param message - The message that was logged (if any)
* @param meta - The metadata object that was logged (if any)
*/
export type PostHook = (
method: string,
err: any,
message: any,
meta: any
) => PromiseLike<any> | any;

export type Options<TLogger extends Logger> = {
/**
* Attempts to parse a boolean value from `process.env.AXE_SHOW_STACK`).
* **If this value is `true`, then if `message` is an instance of an Error,
* it will be invoked as the first argument to logger methods.
* If this is `false`, then only the `err.message` will be invoked as the first argument to logger methods.**
*
* Basically if `true` it will call `logger.method(err)` and if `false` it will call `logger.method(err.message)`.
* If you pass `err` as the first argument to a logger method,
* then it will show the stack trace via `err.stack` typically.
*
* @default true
*/
showStack?: boolean;

meta?: {
/**
* Attempts to parse a boolean value from `process.env.AXE_SHOW_META`
* – meaning you can pass a flag `AXE_SHOW_META=true node app.js` when needed for debugging),
* whether or not to output metadata to logger methods.
* If set to `false`, then fields will not be omitted nor picked;
* the entire meta object will be hidden from logger output.
*
* @default true
*/
show?: boolean;

/**
* Attempts to parse an Object mapping from `process.env.AXE_REMAPPED_META_FIELDS`
* (`,` and `:` delimited, e.g. `REMAPPED_META_FIELDS=foo:bar,beep.boop:beepBoop` to remap `meta.foo` to `meta.bar` and `meta.beep.boop` to `meta.beepBoop`).
* Note that this will clean up empty objects by default unless you set the option `meta.cleanupRemapping` to `false`).
* Supports dot-notation.
*
* @default {}
*/
remappedFields?: RemappedFields;

/**
* Attempts to parse an array value from `process.env.AXE_OMIT_META_FIELDS`
* (`,` delimited) - meaning you can pass a flag `AXE_OMIT_META_FIELDS=user,id node app.js`),
* determining which fields to omit in the metadata passed to logger methods.
* Supports dot-notation.
*
* @default []
*/
omittedFields?: string[];

/**
* Attempts to parse an array value from `process.env.AXE_PICK_META_FIELDS`
* (`,` delimited) - meaning you can pass a flag, e.g. `AXE_PICK_META_FIELDS=request.headers,response.headers node app.js`
* which would pick from `meta.request` and `meta.response` *only* `meta.request.headers` and `meta.response.headers`),
* **This takes precedence after fields are omitted, which means this acts as a whitelist.**
* Supports dot-notation.
* **As of v11.2.0 this now supports Symbols, but only top-level symbols via `Reflect.ownKeys` (not recursive yet).**
*
* @default []
*/
pickedFields?: Array<string | symbol>;

/**
* Whether or not to cleanup empty objects after remapping operations are completed)
*
* @default true
*/
cleanupRemapping?: boolean;

/**
* Whether to suppress HTTP metadata (prevents logger invocation with second arg `meta`)
* if `meta.is_http` is `true` (via [parse-request][] v5.1.0+).
* If you manually set `meta.is_http = true` and this is `true`, then `meta` arg will be suppressed as well.
*
* @default true
*/
hideHTTP?: boolean;

/**
* If this value is provided as a String, then if `meta[config.hideMeta]` is `true`,
* it will suppress the entire metadata object `meta` (the second arg) from being passed/invoked to the logger.
* This is useful when you want to suppress metadata from the logger invocation,
* but still persist it to post hooks (e.g. for sending upstream to your log storage provider).
* This helps to keep development and production console output clean while also allowing you to still store the meta object.
*
* @deafult 'hide_meta'
*/
hideMeta?: string | boolean;
};

/**
* Whether or not to invoke logger methods. Pre and post hooks will still run even if this option is set to `false`.
*
* @default false
*/
silent?: boolean;

/**
* Defaults to `console` with {@link https://github.com/paulmillr/console-polyfill console-polyfill} added automatically, though **you can bring your own logger**.
* See {@link https://github.com/cabinjs/axe?tab=readme-ov-file#custom-logger custom-logger} – you can pass an instance of `pino`, `signale`, `winston`, `bunyan`, etc.
*
* @default console
*/
logger?: TLogger;

/**
* The default name for the logger (defaults to `false` in development environments,
* which does not set `logger.name`)
* – this is useful if you are using a logger like `pino` which prefixes log output with the name set here.
*
* @default `false` if `NODE_ENV` is `"development"` otherwise the value of `process.env.HOSTNAME` or `os.hostname()`
*/
name?: string | boolean;

/**
* The default level of logging to invoke `logger` methods for (defaults to `info`,
* which includes all logs including info and higher in severity (e.g. `info`, `warn`, `error`, `fatal`)
*
* @default 'info'
*/
level?: string;

/**
* An Array of logging levels to support.
* You usually shouldn't change this unless you want to prevent logger methods from being invoked or prevent hooks from being run for a certain log level.
* If an invalid log level is attempted to be invoked, and if it is not in this Array, then no hooks and no logger methods will be invoked.
*
* @default ['info','warn','error','fatal']
*/
levels?: string[];

/**
* Attempts to parse a boolean value from `process.env.AXE_APP_INFO`) - whether or not to parse application information (using [parse-app-info][]).
*
* @default true
*/
appInfo?: boolean;

/**
* See {@link https://github.com/cabinjs/axe?tab=readme-ov-file#hooks Hooks}
*
* @defualt { pre: [], post: [] }
*/
hooks?: {
pre?: PreHook[];
post?: PostHook[];
};
};

type RemappedFields = {
[key: string]: string | RemappedFields;
};
}
29 changes: 19 additions & 10 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ function isFunction(value) {
return typeof value === 'function';
}

function getFunction(value) {
return isFunction(value) ? value : null;
}

class Axe {
// eslint-disable-next-line complexity
constructor(config = {}) {
Expand Down Expand Up @@ -182,16 +186,21 @@ class Axe {
// Bind helper functions for each log level
for (const element of levels) {
// Ensure function exists in logger passed
if (typeof this.config.logger[element] !== 'function') {
if (element === 'fatal') {
this.config.logger.fatal =
this.config.logger.error ||
this.config.logger.info ||
this.config.logger.log;
} else {
this.config.logger[element] =
this.config.logger.info || this.config.logger.log;
}
if (element === 'fatal') {
this.config.logger.fatal =
getFunction(this.config.logger[element]) ||
getFunction(this.config.logger.error) ||
getFunction(this.config.logger.info) ||
getFunction(this.config.logger.log);
} else {
this.config.logger[element] =
getFunction(this.config.logger[element]) ||
getFunction(this.config.logger.info) ||
getFunction(this.config.logger.log);
}

if (!isFunction(this.config.logger[element])) {
throw new Error(`\`${element}\` must be a function on the logger.`);
}

// Bind log handler which normalizes args and populates meta
Expand Down
13 changes: 13 additions & 0 deletions test-d/index.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { expectType, expectNotType } from 'tsd';
import Axe from '../lib';

const logger = new Axe({});

expectType<Axe.Constructor>(Axe);
expectType<Axe.Axe>(logger);

// We can expect that the logger object has all the methods of the Console object except 'config' and 'log' and expected levels
expectType<Console['assert']>(logger.assert);
expectType<Console['count']>(logger.count);
expectType<Console['table']>(logger.table);
expectNotType<Console['log']>(logger.log);
22 changes: 22 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const test = require('ava');
const Axe = require('../lib');

const loggerFnsCheck = test.macro({
exec(t, field) {
t.throws(
() =>
new Axe({
logger: {
[field]: field
}
})
);
},
title(_providedTitle = '', field) {
return `throws if logger fn "${field}" is not a function but is defined`;
}
});

for (const field of ['error', 'info', 'log']) {
test(loggerFnsCheck, field);
}
Loading