Skip to content

Commit

Permalink
Merge pull request #16 from cabinjs/types/init
Browse files Browse the repository at this point in the history
feat: add types and fix minor bug
  • Loading branch information
titanism authored Apr 23, 2024
2 parents 5fa0c58 + 59b2b7a commit 927ea2c
Show file tree
Hide file tree
Showing 6 changed files with 325 additions and 13 deletions.
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);
}

0 comments on commit 927ea2c

Please sign in to comment.