Skip to content

Commit ec50cdf

Browse files
committed
feat: Add global logger object
Fixes #54
1 parent be50968 commit ec50cdf

File tree

7 files changed

+229
-13
lines changed

7 files changed

+229
-13
lines changed

src/lib/core/LocationFull.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { BeforeNavigateEvent, Events, NavigationCancelledEvent, NavigationE
22
import { isConformantState } from "./isConformantState.js";
33
import { LocationLite } from "./LocationLite.svelte.js";
44
import { LocationState } from "./LocationState.svelte.js";
5+
import { logger } from "./Logger.js";
56

67
/**
78
* Location implementation of the library's full mode feature.
@@ -59,7 +60,7 @@ export class LocationFull extends LocationLite {
5960
}
6061
} else {
6162
if (!isConformantState(event.state)) {
62-
console.warn("Warning: Non-conformant state object passed to history." + method + "State. Previous state will prevail.");
63+
logger.warn("Warning: Non-conformant state object passed to history." + method + "State. Previous state will prevail.");
6364
event.state = this.#innerState.state;
6465
}
6566
const navFn = method === 'push' ? this.#originalPushState : this.#originalReplaceState;

src/lib/core/LocationState.svelte.test.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { describe, test, expect, beforeEach, vi } from 'vitest';
22
import { LocationState } from './LocationState.svelte.js';
33
import type { State } from '$lib/types.js';
4+
import { logger } from './Logger.js';
45

5-
// Mock console.warn to capture warnings
6-
const mockConsoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {});
6+
// Mock logger.warn to capture warnings
7+
const mockLoggerWarn = vi.spyOn(logger, 'warn').mockImplementation(() => {});
78

89
// Mock globalThis.window for testing
910
const mockWindow = {
@@ -17,7 +18,7 @@ const mockWindow = {
1718

1819
describe('LocationState', () => {
1920
beforeEach(() => {
20-
mockConsoleWarn.mockClear();
21+
mockLoggerWarn.mockClear();
2122
// Reset to default mock state
2223
mockWindow.history.state = null;
2324
vi.stubGlobal('window', mockWindow);
@@ -37,7 +38,7 @@ describe('LocationState', () => {
3738

3839
// Assert
3940
expect(locationState.state).toStrictEqual(validState); // Deep equality since $state() creates new objects
40-
expect(mockConsoleWarn).not.toHaveBeenCalled();
41+
expect(mockLoggerWarn).not.toHaveBeenCalled();
4142
});
4243

4344
test('Should handle conformant state with undefined path.', () => {
@@ -53,7 +54,7 @@ describe('LocationState', () => {
5354

5455
// Assert
5556
expect(locationState.state).toStrictEqual(validState);
56-
expect(mockConsoleWarn).not.toHaveBeenCalled();
57+
expect(mockLoggerWarn).not.toHaveBeenCalled();
5758
});
5859

5960
test('Should handle conformant state with empty hash object.', () => {
@@ -69,7 +70,7 @@ describe('LocationState', () => {
6970

7071
// Assert
7172
expect(locationState.state).toStrictEqual(validState);
72-
expect(mockConsoleWarn).not.toHaveBeenCalled();
73+
expect(mockLoggerWarn).not.toHaveBeenCalled();
7374
});
7475

7576
test('Should handle conformant state with numeric path.', () => {
@@ -85,7 +86,7 @@ describe('LocationState', () => {
8586

8687
// Assert
8788
expect(locationState.state).toStrictEqual(validState);
88-
expect(mockConsoleWarn).not.toHaveBeenCalled();
89+
expect(mockLoggerWarn).not.toHaveBeenCalled();
8990
});
9091
});
9192

@@ -102,7 +103,7 @@ describe('LocationState', () => {
102103
path: undefined,
103104
hash: {}
104105
});
105-
expect(mockConsoleWarn).not.toHaveBeenCalled();
106+
expect(mockLoggerWarn).not.toHaveBeenCalled();
106107
});
107108

108109
test('Should create clean state when history.state is undefined without warning.', () => {
@@ -117,7 +118,7 @@ describe('LocationState', () => {
117118
path: undefined,
118119
hash: {}
119120
});
120-
expect(mockConsoleWarn).not.toHaveBeenCalled();
121+
expect(mockLoggerWarn).not.toHaveBeenCalled();
121122
});
122123
});
123124

@@ -155,8 +156,8 @@ describe('LocationState', () => {
155156
path: undefined,
156157
hash: {}
157158
});
158-
expect(mockConsoleWarn).toHaveBeenCalledOnce();
159-
expect(mockConsoleWarn).toHaveBeenCalledWith(
159+
expect(mockLoggerWarn).toHaveBeenCalledOnce();
160+
expect(mockLoggerWarn).toHaveBeenCalledWith(
160161
'Non-conformant state data detected in History API. Resetting to clean state.'
161162
);
162163
});

src/lib/core/LocationState.svelte.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { SvelteURL } from "svelte/reactivity";
22
import { isConformantState } from "./isConformantState.js";
3+
import { logger } from "./Logger.js";
34

45
/**
56
* Helper class used to manage the reactive data of Location implementations.
@@ -14,7 +15,7 @@ export class LocationState {
1415
let validState = false;
1516
this.state = $state((validState = isConformantState(historyState)) ? historyState : { path: undefined, hash: {} });
1617
if (!validState && historyState != null) {
17-
console.warn('Non-conformant state data detected in History API. Resetting to clean state.');
18+
logger.warn('Non-conformant state data detected in History API. Resetting to clean state.');
1819
}
1920
}
2021
}

src/lib/core/Logger.test.ts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
2+
import { logger, setLogger } from "./Logger.js";
3+
import type { ILogger } from "$lib/types.js";
4+
5+
describe("logger", () => {
6+
test("Should default to globalThis.console", () => {
7+
expect(logger).toBe(globalThis.console);
8+
});
9+
});
10+
11+
describe("setLogger", () => {
12+
let originalLogger: ILogger;
13+
let mockLogger: ILogger;
14+
let consoleSpy: {
15+
debug: any;
16+
log: any;
17+
warn: any;
18+
error: any;
19+
};
20+
21+
beforeEach(() => {
22+
// Store original logger state
23+
originalLogger = logger;
24+
25+
// Create mock logger
26+
mockLogger = {
27+
debug: vi.fn(),
28+
log: vi.fn(),
29+
warn: vi.fn(),
30+
error: vi.fn()
31+
};
32+
33+
// Create console spies
34+
consoleSpy = {
35+
debug: vi.spyOn(globalThis.console, 'debug').mockImplementation(() => {}),
36+
log: vi.spyOn(globalThis.console, 'log').mockImplementation(() => {}),
37+
warn: vi.spyOn(globalThis.console, 'warn').mockImplementation(() => {}),
38+
error: vi.spyOn(globalThis.console, 'error').mockImplementation(() => {})
39+
};
40+
});
41+
42+
afterEach(() => {
43+
// Restore original state
44+
setLogger(originalLogger);
45+
vi.restoreAllMocks();
46+
});
47+
48+
describe("Boolean arguments", () => {
49+
test("Should set logger to globalThis.console when true", () => {
50+
setLogger(true);
51+
52+
expect(logger).toBe(globalThis.console);
53+
54+
logger.debug("test");
55+
expect(consoleSpy.debug).toHaveBeenCalledWith("test");
56+
});
57+
58+
test("Should set logger to noop functions when false", () => {
59+
setLogger(false);
60+
61+
expect(logger).not.toBe(globalThis.console);
62+
63+
// Should not throw and should not call console
64+
logger.debug("debug message");
65+
logger.log("log message");
66+
logger.warn("warn message");
67+
logger.error("error message");
68+
69+
expect(consoleSpy.debug).not.toHaveBeenCalled();
70+
expect(consoleSpy.log).not.toHaveBeenCalled();
71+
expect(consoleSpy.warn).not.toHaveBeenCalled();
72+
expect(consoleSpy.error).not.toHaveBeenCalled();
73+
});
74+
75+
test("Should allow switching between true and false", () => {
76+
setLogger(true);
77+
logger.log("enabled message");
78+
expect(consoleSpy.log).toHaveBeenCalledWith("enabled message");
79+
80+
setLogger(false);
81+
logger.log("disabled message");
82+
expect(consoleSpy.log).toHaveBeenCalledTimes(1); // Should not be called again
83+
84+
setLogger(true);
85+
logger.log("re-enabled message");
86+
expect(consoleSpy.log).toHaveBeenCalledWith("re-enabled message");
87+
expect(consoleSpy.log).toHaveBeenCalledTimes(2);
88+
});
89+
});
90+
91+
describe("ILogger implementations", () => {
92+
test("Should set custom logger as the global logger", () => {
93+
setLogger(mockLogger);
94+
95+
expect(logger).toBe(mockLogger);
96+
97+
logger.debug("custom debug");
98+
logger.log("custom log");
99+
logger.warn("custom warn");
100+
logger.error("custom error");
101+
102+
expect(mockLogger.debug).toHaveBeenCalledWith("custom debug");
103+
expect(mockLogger.log).toHaveBeenCalledWith("custom log");
104+
expect(mockLogger.warn).toHaveBeenCalledWith("custom warn");
105+
expect(mockLogger.error).toHaveBeenCalledWith("custom error");
106+
107+
// Console should not be called
108+
expect(consoleSpy.debug).not.toHaveBeenCalled();
109+
expect(consoleSpy.log).not.toHaveBeenCalled();
110+
expect(consoleSpy.warn).not.toHaveBeenCalled();
111+
expect(consoleSpy.error).not.toHaveBeenCalled();
112+
});
113+
114+
test("Should work with extended logger implementations", () => {
115+
const extendedLogger = {
116+
...mockLogger,
117+
trace: vi.fn(),
118+
info: vi.fn()
119+
};
120+
121+
setLogger(extendedLogger);
122+
123+
expect(logger).toBe(extendedLogger);
124+
125+
logger.debug("debug");
126+
logger.log("log");
127+
logger.warn("warn");
128+
logger.error("error");
129+
130+
expect(extendedLogger.debug).toHaveBeenCalledWith("debug");
131+
expect(extendedLogger.log).toHaveBeenCalledWith("log");
132+
expect(extendedLogger.warn).toHaveBeenCalledWith("warn");
133+
expect(extendedLogger.error).toHaveBeenCalledWith("error");
134+
});
135+
136+
test("Should allow switching from custom logger back to stock logger", () => {
137+
setLogger(mockLogger);
138+
logger.log("custom message");
139+
expect(mockLogger.log).toHaveBeenCalledWith("custom message");
140+
141+
setLogger(true);
142+
logger.log("stock message");
143+
expect(consoleSpy.log).toHaveBeenCalledWith("stock message");
144+
expect(mockLogger.log).toHaveBeenCalledTimes(1); // Should not be called again
145+
});
146+
147+
test("Should handle multiple parameters correctly", () => {
148+
setLogger(mockLogger);
149+
150+
logger.debug("debug", 123, { key: "value" });
151+
logger.log("log", true, null);
152+
logger.warn("warn", "multiple", "parameters");
153+
logger.error("error", { error: "object" });
154+
155+
expect(mockLogger.debug).toHaveBeenCalledWith("debug", 123, { key: "value" });
156+
expect(mockLogger.log).toHaveBeenCalledWith("log", true, null);
157+
expect(mockLogger.warn).toHaveBeenCalledWith("warn", "multiple", "parameters");
158+
expect(mockLogger.error).toHaveBeenCalledWith("error", { error: "object" });
159+
});
160+
});
161+
});

src/lib/core/Logger.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { ILogger } from "$lib/types.js";
2+
3+
const stockLogger: ILogger = globalThis.console;
4+
5+
const noop = () => { };
6+
7+
const offLogger: ILogger = {
8+
debug: noop,
9+
log: noop,
10+
warn: noop,
11+
error: noop
12+
};
13+
14+
export let logger: ILogger = stockLogger;
15+
16+
export function setLogger(newLogger: boolean | ILogger) {
17+
logger = newLogger === true ? stockLogger : (newLogger === false ? offLogger : newLogger);
18+
};

src/lib/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { setLocation } from "./core/Location.js";
22
import { LocationFull } from "./core/LocationFull.js";
33
import { LocationLite } from "./core/LocationLite.svelte.js";
4+
import { setLogger } from "./core/Logger.js";
45
import { routingOptions, type RoutingOptions } from "./core/options.js";
56
import { setTraceOptions, type TraceOptions } from "./core/trace.svelte.js";
7+
import type { ILogger } from "./types.js";
68

79
/**
810
* Library's initialization options.
@@ -12,6 +14,15 @@ export type InitOptions = RoutingOptions & {
1214
* Tracing options that generally should be off for production builds.
1315
*/
1416
trace?: TraceOptions;
17+
/**
18+
* Controls logging. If `true`, the default logger that logs to the console is used. If `false`, logging is
19+
* turned off. If an object is provided, it is used as the logger.
20+
*
21+
* Logging is turned on by default.
22+
*
23+
* **TIP**: You can provide your own logger implementation to integrate with your application's logging system.
24+
*/
25+
logger?: boolean | ILogger;
1526
}
1627

1728
/**
@@ -30,6 +41,7 @@ export type InitOptions = RoutingOptions & {
3041
*/
3142
export function init(options?: InitOptions): () => void {
3243
setTraceOptions(options?.trace);
44+
setLogger(options?.logger ?? true);
3345
routingOptions.full = options?.full ?? routingOptions.full;
3446
routingOptions.hashMode = options?.hashMode ?? routingOptions.hashMode;
3547
routingOptions.implicitMode = options?.implicitMode ?? routingOptions.implicitMode;

src/lib/types.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,3 +321,25 @@ export type ActiveState = {
321321
* @returns `true` if the fallback content should be shown; `false` to prevent content from being shown.
322322
*/
323323
export type WhenPredicate = (routeStatus: Record<string, RouteStatus>, noMatches: boolean) => boolean;
324+
325+
/**
326+
* Defines the shape of logger objects that can be given to this library during initialization.
327+
*/
328+
export interface ILogger {
329+
/**
330+
* See `console.debug()` for reference.
331+
*/
332+
debug: (...args: any[]) => void;
333+
/**
334+
* See `console.log()` for reference.
335+
*/
336+
log: (...args: any[]) => void;
337+
/**
338+
* See `console.warn()` for reference.
339+
*/
340+
warn: (...args: any[]) => void;
341+
/**
342+
* See `console.error()` for reference.
343+
*/
344+
error: (...args: any[]) => void;
345+
};

0 commit comments

Comments
 (0)