Skip to content

Commit

Permalink
Add logger util (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
mvirgil authored Nov 4, 2024
1 parent a280879 commit 3111261
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 17 deletions.
13 changes: 6 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"author": "",
"license": "Apache-2.0",
"dependencies": {
"chalk": "^4.1.2",
"commander": "^12.1.0"
},
"devDependencies": {
Expand Down
24 changes: 14 additions & 10 deletions src/commands/sourcemaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
*/

import { Command } from 'commander';
import { runSourcemapInject } from '../sourcemaps';
import { debug, error } from '../sourcemaps/utils';
import { runSourcemapInject, SourceMapInjectOptions } from '../sourcemaps';
import { UserFriendlyError } from '../utils/userFriendlyErrors';
import { createLogger, LogLevel } from '../utils/logger';

export const sourcemapsCommand = new Command('sourcemaps');

Expand Down Expand Up @@ -52,24 +52,28 @@ sourcemapsCommand
.description(injectDescription)
.requiredOption(
'--directory <path>',
'Path to the directory containing your both JavaScript files and source map files (required)'
'Path to the directory containing your production JavaScript bundles and their source maps'
)
.option(
'--dry-run',
'Use --dry-run to preview the files that will be injected for the given options, without modifying any files on the file system (optional)',
false
'Use --dry-run to preview the files that will be injected for the given options, without modifying any files on the file system'
)
.option(
'--debug',
'Enable debug logs'
)
.action(
async (options) => {
async (options: SourceMapInjectOptions) => {
const logger = createLogger(options.debug ? LogLevel.DEBUG : LogLevel.INFO);
try {
await runSourcemapInject(options);
} catch (e) {
if (e instanceof UserFriendlyError) {
debug(e.originalError);
error(e.message);
logger.debug(e.originalError);
logger.error(e.message);
} else {
error('Exiting due to an unexpected error:');
error(e);
logger.error('Exiting due to an unexpected error:');
logger.error(e);
}
sourcemapsCommand.error('');
}
Expand Down
1 change: 1 addition & 0 deletions src/sourcemaps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { injectFile } from './injectFile';
export type SourceMapInjectOptions = {
directory: string;
dryRun: boolean;
debug?: boolean;
};

/**
Expand Down
53 changes: 53 additions & 0 deletions src/utils/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright Splunk Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import chalk from 'chalk';

/** Logger methods can be called just like console.log */
export interface Logger {
error: typeof console.log;
warn: typeof console.log;
info: typeof console.log;
debug: typeof console.log;
}

export const enum LogLevel {
ERROR = 4,
WARN = 3,
INFO = 2,
DEBUG = 1
}

export function createLogger(logLevel: LogLevel): Logger {
// Send info to stdout, and all other logs to stderr
return {
error: (msg, ...params) => LogLevel.ERROR >= logLevel && prefixedConsoleError(chalk.stderr.red('ERROR '), msg, ...params),
warn: (msg, ...params) => LogLevel.WARN >= logLevel && prefixedConsoleError(chalk.stderr.yellow('WARN '), msg, ...params),
info: (msg, ...params) => LogLevel.INFO >= logLevel && console.log(msg, params),
debug: (msg, ...params) => LogLevel.DEBUG >= logLevel && prefixedConsoleError(chalk.stderr.gray('DEBUG '), msg, ...params),
} as Logger;
}

/** Carefully wrap console.error so the logger can properly support format strings */
const prefixedConsoleError = (prefix: string, msg: unknown, ...params: unknown[]) => {
if (typeof msg === 'string') {
// String concatenation is needed for format strings,
// otherwise console.error('Hello ', '%s!', ' World') would print 'Hello %s! World', not 'Hello World!'
console.error(`${prefix}${msg}`, ...params);
} else {
console.error(prefix, msg, ...params);
}
};
86 changes: 86 additions & 0 deletions test/utils/logger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright Splunk Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { describe, it, mock } from 'node:test';
import { createLogger, LogLevel } from '../../src/utils/logger';
import { equal, match } from 'node:assert/strict';

describe('createLogger', () => {

it('should respect log level', () => {
const output: unknown[] = [];
mock.method(console, 'log', (arg: unknown) => output.push(arg));
mock.method(console, 'error', (arg: unknown) => output.push(arg));

const levels = new Map([
[ LogLevel.ERROR, 'error' ],
[ LogLevel.WARN, 'warn' ],
[ LogLevel.INFO, 'info' ],
[ LogLevel.DEBUG, 'debug' ],
]);

for (const [ level, label ] of levels.entries()) {
const logger = createLogger(level);
logger.error(`${label}.error`);
logger.warn(`${label}.warn`);
logger.info(`${label}.info`);
logger.debug(`${label}.debug`);
}

const lines = output.join('\n');

equal(lines.includes('debug.error'), true);
equal(lines.includes('debug.warn'), true);
equal(lines.includes('debug.info'), true);
equal(lines.includes('debug.debug'), true);

equal(lines.includes('info.error'), true);
equal(lines.includes('info.warn'), true);
equal(lines.includes('info.info'), true);
equal(lines.includes('info.debug'), false);

equal(lines.includes('warn.error'), true);
equal(lines.includes('warn.warn'), true);
equal(lines.includes('warn.info'), false);
equal(lines.includes('warn.debug'), false);

equal(lines.includes('error.error'), true);
equal(lines.includes('error.warn'), false);
equal(lines.includes('error.info'), false);
equal(lines.includes('error.debug'), false);
});

it('should not try to concatenate error objects with the prefix string', () => {
const consoleErrorMock = mock.method(console, 'error', () => {});

const logger = createLogger(LogLevel.DEBUG);
const err = new Error('error');
logger.error(err);

equal(consoleErrorMock.mock.calls[0].arguments[1], err);
});

it('should support format functions like console.log does', () => {
const consoleErrorMock = mock.method(console, 'error', () => {});

const logger = createLogger(LogLevel.DEBUG);
logger.debug('hello %s', 'world');

match(consoleErrorMock.mock.calls[0].arguments[0], /hello %s/);
equal(consoleErrorMock.mock.calls[0].arguments[1], 'world');
});

});

0 comments on commit 3111261

Please sign in to comment.