Skip to content

Commit

Permalink
feat: configurable console object and documentation (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
eliassjogreen authored Mar 7, 2024
1 parent 67aa94e commit b592459
Show file tree
Hide file tree
Showing 11 changed files with 239 additions and 35 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ If you would like a prettier package name I would suggest changing it in the

</details>

### [JSON](./examples/json.js)
### [JSON](./examples/json/json.js)

The following example demonstrates how to capture and log messages as JSON to
stdout.
Expand Down Expand Up @@ -128,7 +128,7 @@ console.groupEnd();
console.info("Info message");
```

### [Teeing](./examples/tee.js)
### [Teeing](./examples/tee/tee.js)

Sometimes you may want to log messages to multiple destinations. This can be
done using
Expand Down
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@denosaurs/log",
"version": "0.0.8",
"version": "0.0.9",
"exports": {
".": "./mod.ts",
"./transforms/filter": "./transforms/filter.ts",
Expand Down
2 changes: 1 addition & 1 deletion examples/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@
"@std/json": "jsr:@std/json"
},
"tasks": {
"prepare": "npm install --force && deno cache -r json.js && deno cache -r tee.js"
"prepare": "npm install --force && deno cache -r **/*.js && deno cache -r **/*.ts"
}
}
File renamed without changes.
2 changes: 1 addition & 1 deletion examples/json.test.ts → examples/json/json.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { assert } from "jsr:@std/assert";
import { assertIsLog, createJSONLineStream } from "./utils.ts";
import { assertIsLog, createJSONLineStream } from "../utils.ts";

Deno.test("logs to stdout in json example", async ({ step }) => {
await step("deno", async () => {
Expand Down
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion examples/tee.test.ts → examples/tee/tee.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { assert, assertStrictEquals } from "jsr:@std/assert";
import { assertCollectLogLines } from "./utils.ts";
import { assertCollectLogLines } from "../utils.ts";

async function assertStdout(stdout: Uint8Array) {
const logs = await assertCollectLogLines(stdout);
Expand Down
232 changes: 204 additions & 28 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,164 @@
/**
* A copy of the original console object.
* Create a function that restores the given console to its original state.
*/
export const originalConsole: Console = {
assert: console?.assert.bind(console),
clear: console?.clear.bind(console),
count: console?.count.bind(console),
countReset: console?.countReset.bind(console),
debug: console?.debug.bind(console),
dir: console?.dir.bind(console),
dirxml: console?.dirxml.bind(console),
error: console?.error.bind(console),
group: console?.group.bind(console),
groupCollapsed: console?.groupCollapsed.bind(console),
groupEnd: console?.groupEnd.bind(console),
info: console?.info.bind(console),
log: console?.log.bind(console),
table: console?.table.bind(console),
time: console?.time.bind(console),
timeEnd: console?.timeEnd.bind(console),
timeLog: console?.timeLog.bind(console),
trace: console?.trace.bind(console),
warn: console?.warn.bind(console),
timeStamp: console?.timeStamp.bind(console),
profile: console?.profile.bind(console),
profileEnd: console?.profileEnd.bind(console),
};
function createRestoreConsole(original: Console) {
const methods: PropertyDescriptorMap = {
assert: {
value: original?.assert.bind(original),
writable: true,
enumerable: true,
configurable: true,
},
clear: {
value: original?.clear.bind(original),
writable: true,
enumerable: true,
configurable: true,
},
count: {
value: original?.count.bind(original),
writable: true,
enumerable: true,
configurable: true,
},
countReset: {
value: original?.countReset.bind(original),
writable: true,
enumerable: true,
configurable: true,
},
debug: {
value: original?.debug.bind(original),
writable: true,
enumerable: true,
configurable: true,
},
dir: {
value: original?.dir.bind(original),
writable: true,
enumerable: true,
configurable: true,
},
dirxml: {
value: original?.dirxml.bind(original),
writable: true,
enumerable: true,
configurable: true,
},
error: {
value: original?.error.bind(original),
writable: true,
enumerable: true,
configurable: true,
},
group: {
value: original?.group.bind(original),
writable: true,
enumerable: true,
configurable: true,
},
groupCollapsed: {
value: original?.groupCollapsed.bind(original),
writable: true,
enumerable: true,
configurable: true,
},
groupEnd: {
value: original?.groupEnd.bind(original),
writable: true,
enumerable: true,
configurable: true,
},
info: {
value: original?.info.bind(original),
writable: true,
enumerable: true,
configurable: true,
},
log: {
value: original?.log.bind(original),
writable: true,
enumerable: true,
configurable: true,
},
table: {
value: original?.table.bind(original),
writable: true,
enumerable: true,
configurable: true,
},
time: {
value: original?.time.bind(original),
writable: true,
enumerable: true,
configurable: true,
},
timeEnd: {
value: original?.timeEnd.bind(original),
writable: true,
enumerable: true,
configurable: true,
},
timeLog: {
value: original?.timeLog.bind(original),
writable: true,
enumerable: true,
configurable: true,
},
trace: {
value: original?.trace.bind(original),
writable: true,
enumerable: true,
configurable: true,
},
warn: {
value: original?.warn.bind(original),
writable: true,
enumerable: true,
configurable: true,
},
timeStamp: {
value: original?.timeStamp.bind(original),
writable: true,
enumerable: true,
configurable: true,
},
profile: {
value: original?.profile.bind(original),
writable: true,
enumerable: true,
configurable: true,
},
profileEnd: {
value: original?.profileEnd.bind(original),
writable: true,
enumerable: true,
configurable: true,
},
};

return (console: Console) => Object.defineProperties(console, methods);
}

type RecursivePartial<T> = {
[P in keyof T]?: RecursivePartial<T[P]>;
};

/**
* All of the log levels that are supported by the console.
*/
export type LogLevel =
| "trace"
| "debug"
| "info"
| "warn"
| "error";

/**
* A numeric representation of the log levels to be able to compare log levels.
* The lower the number, the more verbose the log level. The higher the number,
* the more severe the log level.
*/
export const LOG_LEVELS: { [level in LogLevel]: number } = {
"trace": 1,
"debug": 2,
Expand All @@ -51,14 +173,52 @@ export const LOG_LEVELS: { [level in LogLevel]: number } = {
export interface Log {
/** A millisecond resolution unix timestamp of when the log was made */
timestamp: number;
/** The log level of the log. */
level: LogLevel;
/**
* An array of groups that were active when the log was made. The groups are
* a clone of the array of all the arguments that were passed to the console
* `group` and `groupCollapsed` methods as to never modify any potential
* references.
*
* @example
* ```ts
* console.group("group1", "group2");
* console.group("group3");
* console.log("Hello, world!");
* // Would result in the following groups:
* // [["group1", "group2"], ["group3"]]
* ```
*/
// deno-lint-ignore no-explicit-any
groups: any[];
groups: any[][];
/**
* The data that was logged. It is a clone of the array of all the arguments
* that were passed to the console method which made the log as to never
* modify any potential references.
*/
// deno-lint-ignore no-explicit-any
data: any[];
}

/**
* Options for the {@link ConsoleReadableStream}.
*/
export interface ConsoleReadableStreamOptions {
/**
* The console object to capture logs from. The object will be modified to
* capture all logs and will be restored to its original state when the
* stream is closed.
*
* @default globalThis.console
*/
console: Console;
/**
* Internal methods and configuration. Depending on the options, the
* performance, supported features and timestamp accuracy may vary.
*
* @default defaultConsoleReadableStreamOptions
*/
internals: {
/**
* @returns A millisecond resolution unix timestamp of when the log was made
Expand All @@ -78,6 +238,7 @@ export interface ConsoleReadableStreamOptions {
*/
export const defaultConsoleReadableStreamOptions: ConsoleReadableStreamOptions =
{
console: globalThis.console,
internals: {
now: Date.now,
clone: <T>(data: T) => {
Expand All @@ -104,6 +265,7 @@ export const defaultConsoleReadableStreamOptions: ConsoleReadableStreamOptions =
* {@link defaultConsoleReadableStreamOptions} instead.
*/
export const fastConsoleReadableStreamOptions: ConsoleReadableStreamOptions = {
console: globalThis.console,
internals: {
now: Date.now,
clone: <T>(data: T) => {
Expand All @@ -126,6 +288,7 @@ export const fastConsoleReadableStreamOptions: ConsoleReadableStreamOptions = {
*/
export const hrtimeConsoleReadableStreamOptions: ConsoleReadableStreamOptions =
{
console: globalThis.console,
internals: {
now: () => performance.timeOrigin + performance.now(),
clone: <T>(data: T) => {
Expand All @@ -148,12 +311,15 @@ export const hrtimeConsoleReadableStreamOptions: ConsoleReadableStreamOptions =
*/
export class ConsoleReadableStream extends ReadableStream<Log> {
#options: ConsoleReadableStreamOptions;
#restore: (console: Console) => Console;
#close: () => void;

constructor(
options: RecursivePartial<ConsoleReadableStreamOptions> =
defaultConsoleReadableStreamOptions,
) {
options ??= defaultConsoleReadableStreamOptions;
options.console ??= defaultConsoleReadableStreamOptions.console;
options.internals ??= defaultConsoleReadableStreamOptions.internals;
options.internals.now ??= defaultConsoleReadableStreamOptions.internals.now;
options.internals.clone ??=
Expand All @@ -170,6 +336,8 @@ export class ConsoleReadableStream extends ReadableStream<Log> {
});

this.#options = options as ConsoleReadableStreamOptions;
this.#restore = createRestoreConsole(this.#options.console);
this.#close = () => controller.close();

// deno-lint-ignore no-explicit-any
const wrapper = (level: LogLevel) => (...data: any[]) => {
Expand All @@ -181,7 +349,7 @@ export class ConsoleReadableStream extends ReadableStream<Log> {
});
};

Object.defineProperties(globalThis.console, {
Object.defineProperties(options.console, {
trace: {
value: wrapper("trace"),
writable: true,
Expand Down Expand Up @@ -248,9 +416,17 @@ export class ConsoleReadableStream extends ReadableStream<Log> {
});
}

/**
* Closes the stream and restores the original console object.
*/
close() {
this.#close();
this.#options.console = this.#restore(this.#options.console);
}

// deno-lint-ignore no-explicit-any
cancel(reason?: any): Promise<void> {
globalThis.console = originalConsole;
this.close();
return super.cancel(reason);
}
}
27 changes: 27 additions & 0 deletions utils/original_console.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* A copy of the original {@link console} object.
*/
export const originalConsole: Console = {
assert: console?.assert.bind(console),
clear: console?.clear.bind(console),
count: console?.count.bind(console),
countReset: console?.countReset.bind(console),
debug: console?.debug.bind(console),
dir: console?.dir.bind(console),
dirxml: console?.dirxml.bind(console),
error: console?.error.bind(console),
group: console?.group.bind(console),
groupCollapsed: console?.groupCollapsed.bind(console),
groupEnd: console?.groupEnd.bind(console),
info: console?.info.bind(console),
log: console?.log.bind(console),
table: console?.table.bind(console),
time: console?.time.bind(console),
timeEnd: console?.timeEnd.bind(console),
timeLog: console?.timeLog.bind(console),
trace: console?.trace.bind(console),
warn: console?.warn.bind(console),
timeStamp: console?.timeStamp.bind(console),
profile: console?.profile.bind(console),
profileEnd: console?.profileEnd.bind(console),
};
Loading

0 comments on commit b592459

Please sign in to comment.