@real-router/logger-plugin 0.2.24 → 0.2.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@real-router/logger-plugin",
3
- "version": "0.2.24",
3
+ "version": "0.2.26",
4
4
  "type": "commonjs",
5
5
  "description": "Development logging plugin with transition tracking and performance metrics",
6
6
  "main": "./dist/cjs/index.js",
@@ -8,6 +8,7 @@
8
8
  "types": "./dist/esm/index.d.mts",
9
9
  "exports": {
10
10
  ".": {
11
+ "development": "./src/index.ts",
11
12
  "types": {
12
13
  "import": "./dist/esm/index.d.mts",
13
14
  "require": "./dist/cjs/index.d.ts"
@@ -17,7 +18,8 @@
17
18
  }
18
19
  },
19
20
  "files": [
20
- "dist"
21
+ "dist",
22
+ "src"
21
23
  ],
22
24
  "repository": {
23
25
  "type": "git",
@@ -43,7 +45,7 @@
43
45
  "homepage": "https://github.com/greydragon888/real-router",
44
46
  "sideEffects": false,
45
47
  "dependencies": {
46
- "@real-router/core": "^0.21.0",
48
+ "@real-router/core": "^0.23.0",
47
49
  "@real-router/logger": "^0.2.0"
48
50
  },
49
51
  "scripts": {
@@ -0,0 +1,11 @@
1
+ // packages/logger-plugin/modules/constants.ts
2
+
3
+ import type { LoggerPluginConfig } from "./types";
4
+
5
+ export const DEFAULT_CONFIG: Required<LoggerPluginConfig> = {
6
+ level: "all",
7
+ usePerformanceMarks: false,
8
+ showParamsDiff: true,
9
+ showTiming: true,
10
+ context: "logger-plugin",
11
+ };
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ // packages/logger-plugin/modules/index.ts
2
+
3
+ // Public API exports for real-router-logger-plugin
4
+
5
+ // Main plugin factory and instance
6
+ export { loggerPluginFactory, loggerPlugin } from "./plugin";
7
+
8
+ // Types
9
+ export type { LoggerPluginConfig, LogLevel } from "./types";
@@ -0,0 +1,74 @@
1
+ // packages/logger-plugin/modules/internal/console-groups.ts
2
+
3
+ /**
4
+ * Checks if console.group is supported in the current environment.
5
+ */
6
+ export const supportsConsoleGroups = (): boolean => {
7
+ return (
8
+ typeof console !== "undefined" &&
9
+ typeof console.group === "function" &&
10
+ typeof console.groupEnd === "function"
11
+ );
12
+ };
13
+
14
+ /**
15
+ * Manager for handling console groups
16
+ */
17
+ interface GroupManager {
18
+ /**
19
+ * Opens a group if it's not already open
20
+ */
21
+ open: (label: string) => void;
22
+ /**
23
+ * Closes a group if it's open
24
+ */
25
+ close: () => void;
26
+ /**
27
+ * Checks if a group is currently open
28
+ */
29
+ isOpen: () => boolean;
30
+ }
31
+
32
+ /**
33
+ * Creates a manager for handling console groups.
34
+ * Prevents duplicate group opening.
35
+ *
36
+ * @param enabled - Whether groups are supported in the environment
37
+ * @returns Object with open and close methods
38
+ */
39
+ export const createGroupManager = (enabled: boolean): GroupManager => {
40
+ let isOpened = false;
41
+
42
+ return {
43
+ /**
44
+ * Opens a group if it's not already open.
45
+ */
46
+ open(label: string): void {
47
+ if (!enabled || isOpened) {
48
+ return;
49
+ }
50
+
51
+ console.group(label);
52
+ isOpened = true;
53
+ },
54
+
55
+ /**
56
+ * Closes a group if it's open.
57
+ */
58
+ close(): void {
59
+ if (!enabled || !isOpened) {
60
+ return;
61
+ }
62
+
63
+ console.groupEnd();
64
+ isOpened = false;
65
+ },
66
+
67
+ /**
68
+ * Checks if a group is currently open.
69
+ */
70
+ isOpen(): boolean {
71
+ return isOpened;
72
+ },
73
+ };
74
+ };
@@ -0,0 +1,56 @@
1
+ // packages/logger-plugin/modules/internal/formatting.ts
2
+
3
+ import type { State } from "@real-router/core";
4
+
5
+ /**
6
+ * Formats route name for logging output.
7
+ * Handles undefined/null.
8
+ */
9
+ export const formatRouteName = (state?: State): string => {
10
+ return state?.name ?? "(none)";
11
+ };
12
+
13
+ /**
14
+ * Formats execution time information.
15
+ * Uses adaptive units:
16
+ * - Microseconds (μs) for <0.1ms
17
+ * - Milliseconds (ms) for ≥0.1ms
18
+ *
19
+ * @param startTime - Start time or null
20
+ * @param now - Function to get current time
21
+ * @returns String with time or empty string
22
+ */
23
+ export const formatTiming = (
24
+ startTime: number | null,
25
+ now: () => number,
26
+ ): string => {
27
+ if (startTime === null) {
28
+ return "";
29
+ }
30
+
31
+ const durationMs = now() - startTime;
32
+
33
+ if (!Number.isFinite(durationMs) || durationMs < 0) {
34
+ return " (?)";
35
+ }
36
+
37
+ if (durationMs < 0.1) {
38
+ const durationMks = (durationMs * 1000).toFixed(2);
39
+
40
+ return ` (${durationMks}μs)`;
41
+ } else {
42
+ const duration = durationMs.toFixed(2);
43
+
44
+ return ` (${duration}ms)`;
45
+ }
46
+ };
47
+
48
+ /**
49
+ * Creates a label for Performance API from route names.
50
+ */
51
+ export const createTransitionLabel = (
52
+ fromRoute: string,
53
+ toRoute: string,
54
+ ): string => {
55
+ return `${fromRoute}→${toRoute}`;
56
+ };
@@ -0,0 +1,95 @@
1
+ // packages/logger-plugin/modules/internal/params-diff.ts
2
+
3
+ import type { Params } from "@real-router/core";
4
+
5
+ export interface ParamsDiff {
6
+ changed: Record<string, { from: unknown; to: unknown }>;
7
+ added: Record<string, unknown>;
8
+ removed: Record<string, unknown>;
9
+ }
10
+
11
+ /**
12
+ * Calculates differences between two parameter objects.
13
+ * Performs only shallow comparison.
14
+ *
15
+ * @param fromParams - Previous parameters
16
+ * @param toParams - New parameters
17
+ * @returns Object with differences or null if there are no changes
18
+ */
19
+ export const getParamsDiff = (
20
+ fromParams: Params,
21
+ toParams: Params,
22
+ ): ParamsDiff | null => {
23
+ const changed: ParamsDiff["changed"] = {};
24
+ const added: ParamsDiff["added"] = {};
25
+ const removed: ParamsDiff["removed"] = {};
26
+
27
+ // Track if any changes found to avoid iterating through objects at the end.
28
+ // This is a performance optimization: instead of calling Object.keys().length
29
+ // three times to check if objects are empty, we set this flag when we find
30
+ // any change and check it once at the end.
31
+ let hasChanges = false;
32
+
33
+ // Find changed and removed
34
+ for (const key in fromParams) {
35
+ if (!(key in toParams)) {
36
+ removed[key] = fromParams[key];
37
+ hasChanges = true;
38
+ } else if (fromParams[key] !== toParams[key]) {
39
+ changed[key] = { from: fromParams[key], to: toParams[key] };
40
+ hasChanges = true;
41
+ }
42
+ }
43
+
44
+ // Find added
45
+ for (const key in toParams) {
46
+ if (!(key in fromParams)) {
47
+ added[key] = toParams[key];
48
+ hasChanges = true;
49
+ }
50
+ }
51
+
52
+ // Return null if there are no changes
53
+ if (!hasChanges) {
54
+ return null;
55
+ }
56
+
57
+ return { changed, added, removed };
58
+ };
59
+
60
+ /**
61
+ * Formats and logs parameter differences.
62
+ *
63
+ * @param diff - Object with differences
64
+ * @param context - Context for console
65
+ */
66
+ export const logParamsDiff = (diff: ParamsDiff, context: string): void => {
67
+ const parts: string[] = [];
68
+
69
+ // Cache entries to avoid double iteration
70
+ const changedEntries = Object.entries(diff.changed);
71
+
72
+ if (changedEntries.length > 0) {
73
+ const items: string[] = [];
74
+
75
+ for (const [key, { from, to }] of changedEntries) {
76
+ items.push(`${key}: ${JSON.stringify(from)} → ${JSON.stringify(to)}`);
77
+ }
78
+
79
+ parts.push(`Changed: { ${items.join(", ")} }`);
80
+ }
81
+
82
+ const addedEntries = Object.entries(diff.added);
83
+
84
+ if (addedEntries.length > 0) {
85
+ parts.push(`Added: ${JSON.stringify(diff.added)}`);
86
+ }
87
+
88
+ const removedEntries = Object.entries(diff.removed);
89
+
90
+ if (removedEntries.length > 0) {
91
+ parts.push(`Removed: ${JSON.stringify(diff.removed)}`);
92
+ }
93
+
94
+ console.log(`[${context}] ${parts.join(", ")}`);
95
+ };
@@ -0,0 +1,67 @@
1
+ // packages/logger-plugin/modules/internal/performance-marks.ts
2
+
3
+ /**
4
+ * Checks if Performance API is supported in the current environment.
5
+ */
6
+ export const supportsPerformanceAPI = (): boolean => {
7
+ return (
8
+ typeof performance !== "undefined" &&
9
+ typeof performance.mark === "function" &&
10
+ typeof performance.measure === "function"
11
+ );
12
+ };
13
+
14
+ /**
15
+ * Performance tracker interface with mark and measure methods.
16
+ */
17
+ interface PerformanceTracker {
18
+ mark: (name: string) => void;
19
+ measure: (measureName: string, startMark: string, endMark: string) => void;
20
+ }
21
+
22
+ /**
23
+ * Creates a tracker for working with the Performance API.
24
+ * Ignores calls if the API is unavailable.
25
+ *
26
+ * @param enabled - Whether the functionality is enabled (from config)
27
+ * @param context - Context for error logging
28
+ * @returns Object with mark and measure methods
29
+ */
30
+ export const createPerformanceTracker = (
31
+ enabled: boolean,
32
+ context: string,
33
+ ): PerformanceTracker => {
34
+ const isSupported = enabled && supportsPerformanceAPI();
35
+
36
+ return {
37
+ /**
38
+ * Creates a performance mark with the specified name.
39
+ */
40
+ mark(name: string): void {
41
+ if (!isSupported) {
42
+ return;
43
+ }
44
+
45
+ performance.mark(name);
46
+ },
47
+
48
+ /**
49
+ * Creates a performance measure between two marks.
50
+ * Logs a warning if the marks don't exist.
51
+ */
52
+ measure(measureName: string, startMark: string, endMark: string): void {
53
+ if (!isSupported) {
54
+ return;
55
+ }
56
+
57
+ try {
58
+ performance.measure(measureName, startMark, endMark);
59
+ } catch (error) {
60
+ console.warn(
61
+ `[${context}] Failed to create performance measure: ${measureName}`,
62
+ error,
63
+ );
64
+ }
65
+ },
66
+ };
67
+ };
@@ -0,0 +1,52 @@
1
+ // packages/logger-plugin/modules/internal/timing.ts
2
+
3
+ /**
4
+ * Function that returns high-resolution timestamp in milliseconds.
5
+ */
6
+ type TimeProvider = () => number;
7
+
8
+ /**
9
+ * State for Date.now() monotonic emulation
10
+ */
11
+ let lastTimestamp = 0;
12
+ let timeOffset = 0;
13
+
14
+ /**
15
+ * Creates monotonic Date.now() wrapper that ensures time never goes backwards.
16
+ *
17
+ * @returns Time provider function with monotonic guarantee
18
+ */
19
+ function createMonotonicDateNow(): TimeProvider {
20
+ // eslint-disable-next-line unicorn/consistent-function-scoping -- closure over module-level lastTimestamp/timeOffset
21
+ return (): number => {
22
+ const current: number = Date.now();
23
+
24
+ if (current < lastTimestamp) {
25
+ timeOffset += lastTimestamp - current;
26
+ }
27
+
28
+ lastTimestamp = current;
29
+
30
+ return current + timeOffset;
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Initialize time provider based on environment.
36
+ * Uses performance.now() in modern environments (Node.js 16+, all browsers),
37
+ * falls back to monotonic Date.now() wrapper for edge cases.
38
+ */
39
+ const nowFn: TimeProvider =
40
+ typeof performance !== "undefined" && typeof performance.now === "function"
41
+ ? (): number => performance.now()
42
+ : createMonotonicDateNow();
43
+
44
+ /**
45
+ * Returns high-resolution monotonic timestamp.
46
+ *
47
+ * Uses performance.now() in modern environments (Node.js 16+, all browsers).
48
+ * Falls back to monotonic Date.now() wrapper (~1ms precision) for edge cases.
49
+ *
50
+ * @returns Timestamp in milliseconds
51
+ */
52
+ export const now = (): number => nowFn();
package/src/plugin.ts ADDED
@@ -0,0 +1,266 @@
1
+ // packages/logger-plugin/modules/plugin.ts
2
+
3
+ import { DEFAULT_CONFIG } from "./constants";
4
+ import {
5
+ createGroupManager,
6
+ supportsConsoleGroups,
7
+ } from "./internal/console-groups";
8
+ import {
9
+ formatRouteName,
10
+ formatTiming,
11
+ createTransitionLabel,
12
+ } from "./internal/formatting";
13
+ import { getParamsDiff, logParamsDiff } from "./internal/params-diff";
14
+ import { createPerformanceTracker } from "./internal/performance-marks";
15
+ import { now } from "./internal/timing";
16
+
17
+ import type { LoggerPluginConfig, LogLevel } from "./types";
18
+ import type { PluginFactory, RouterError, State } from "@real-router/core";
19
+
20
+ /**
21
+ * Checks if the given log type should be output based on the configured level.
22
+ *
23
+ * Level hierarchy:
24
+ * - 'all': log everything (start, stop, transitions, warnings, errors)
25
+ * - 'transitions': only transition events (start, success, cancel, error)
26
+ * - 'errors': only errors
27
+ * - 'none': nothing
28
+ */
29
+ const shouldLog = (
30
+ level: LogLevel,
31
+ type: "lifecycle" | "transition" | "warning" | "error",
32
+ ): boolean => {
33
+ if (level === "none") {
34
+ return false;
35
+ }
36
+
37
+ if (level === "errors") {
38
+ return type === "error";
39
+ }
40
+
41
+ if (level === "transitions") {
42
+ return type === "transition" || type === "warning" || type === "error";
43
+ }
44
+
45
+ // level === 'all'
46
+ return true;
47
+ };
48
+
49
+ /**
50
+ * Creates a logger-plugin for real-router.
51
+ *
52
+ * @param options - Plugin configuration options
53
+ * @returns Plugin factory function for real-router
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * import { loggerPluginFactory } from "@real-router/logger-plugin";
58
+ *
59
+ * // Use with default configuration
60
+ * router.usePlugin(loggerPluginFactory());
61
+ *
62
+ * // Use with custom configuration
63
+ * router.usePlugin(loggerPluginFactory({
64
+ * level: 'errors', // only log errors
65
+ * usePerformanceMarks: true, // enable Performance API
66
+ * showTiming: false, // disable timing info
67
+ * showParamsDiff: false, // disable params diff
68
+ * context: 'my-router', // custom context name
69
+ * }));
70
+ * ```
71
+ */
72
+ export function loggerPluginFactory(
73
+ options?: Partial<LoggerPluginConfig>,
74
+ ): PluginFactory {
75
+ // Merge options with defaults
76
+ const config: Required<LoggerPluginConfig> = {
77
+ ...DEFAULT_CONFIG,
78
+ ...options,
79
+ };
80
+
81
+ return () => {
82
+ // Create helper managers
83
+ const groups = createGroupManager(supportsConsoleGroups());
84
+ const perf = createPerformanceTracker(
85
+ config.usePerformanceMarks,
86
+ config.context,
87
+ );
88
+
89
+ // Transition state
90
+ let transitionStartTime: number | null = null;
91
+
92
+ /**
93
+ * Logs parameter differences when navigating within the same route.
94
+ */
95
+ const logParamsIfNeeded = (toState: State, fromState?: State): void => {
96
+ if (!config.showParamsDiff || !fromState) {
97
+ return;
98
+ }
99
+
100
+ // Show diff only for the same route
101
+ if (toState.name !== fromState.name) {
102
+ return;
103
+ }
104
+
105
+ const diff = getParamsDiff(fromState.params, toState.params);
106
+
107
+ if (diff) {
108
+ logParamsDiff(diff, config.context);
109
+ }
110
+ };
111
+
112
+ /**
113
+ * Formats timing string based on config.
114
+ */
115
+ const getTiming = (): string => {
116
+ if (!config.showTiming) {
117
+ return "";
118
+ }
119
+
120
+ return formatTiming(transitionStartTime, now);
121
+ };
122
+
123
+ return {
124
+ onStart() {
125
+ perf.mark("router:start");
126
+
127
+ if (shouldLog(config.level, "lifecycle")) {
128
+ console.log(`[${config.context}] Router started`);
129
+ }
130
+ },
131
+
132
+ onStop() {
133
+ groups.close();
134
+
135
+ perf.mark("router:stop");
136
+ perf.measure("router:lifetime", "router:start", "router:stop");
137
+
138
+ if (shouldLog(config.level, "lifecycle")) {
139
+ console.log(`[${config.context}] Router stopped`);
140
+ }
141
+ },
142
+
143
+ onTransitionStart(toState: State, fromState?: State) {
144
+ groups.open("Router transition");
145
+ transitionStartTime = now();
146
+
147
+ const fromRoute = formatRouteName(fromState);
148
+ const toRoute = formatRouteName(toState);
149
+ const label = createTransitionLabel(fromRoute, toRoute);
150
+
151
+ perf.mark(`router:transition-start:${label}`);
152
+
153
+ if (shouldLog(config.level, "transition")) {
154
+ console.log(
155
+ `[${config.context}] Transition: ${fromRoute} → ${toRoute}`,
156
+ {
157
+ from: fromState,
158
+ to: toState,
159
+ },
160
+ );
161
+
162
+ logParamsIfNeeded(toState, fromState);
163
+ }
164
+ },
165
+
166
+ onTransitionSuccess(toState: State, fromState?: State) {
167
+ const fromRoute = formatRouteName(fromState);
168
+ const toRoute = formatRouteName(toState);
169
+ const label = createTransitionLabel(fromRoute, toRoute);
170
+
171
+ perf.mark(`router:transition-end:${label}`);
172
+ perf.measure(
173
+ `router:transition:${label}`,
174
+ `router:transition-start:${label}`,
175
+ `router:transition-end:${label}`,
176
+ );
177
+
178
+ if (shouldLog(config.level, "transition")) {
179
+ const timing = getTiming();
180
+
181
+ console.log(`[${config.context}] Transition success${timing}`, {
182
+ to: toState,
183
+ from: fromState,
184
+ });
185
+ }
186
+
187
+ groups.close();
188
+ transitionStartTime = null;
189
+ },
190
+
191
+ onTransitionCancel(toState: State, fromState?: State) {
192
+ const fromRoute = formatRouteName(fromState);
193
+ const toRoute = formatRouteName(toState);
194
+ const label = createTransitionLabel(fromRoute, toRoute);
195
+
196
+ perf.mark(`router:transition-cancel:${label}`);
197
+ perf.measure(
198
+ `router:transition-cancelled:${label}`,
199
+ `router:transition-start:${label}`,
200
+ `router:transition-cancel:${label}`,
201
+ );
202
+
203
+ if (shouldLog(config.level, "warning")) {
204
+ const timing = getTiming();
205
+
206
+ console.warn(`[${config.context}] Transition cancelled${timing}`, {
207
+ to: toState,
208
+ from: fromState,
209
+ });
210
+ }
211
+
212
+ groups.close();
213
+ transitionStartTime = null;
214
+ },
215
+
216
+ onTransitionError(
217
+ toState: State | undefined,
218
+ fromState: State | undefined,
219
+ err: RouterError,
220
+ ) {
221
+ const fromRoute = formatRouteName(fromState);
222
+ const toRoute = formatRouteName(toState);
223
+ const label = createTransitionLabel(fromRoute, toRoute);
224
+
225
+ perf.mark(`router:transition-error:${label}`);
226
+ perf.measure(
227
+ `router:transition-failed:${label}`,
228
+ `router:transition-start:${label}`,
229
+ `router:transition-error:${label}`,
230
+ );
231
+
232
+ if (shouldLog(config.level, "error")) {
233
+ const timing = getTiming();
234
+
235
+ console.error(
236
+ `[${config.context}] Transition error: ${err.code}${timing}`,
237
+ {
238
+ error: err,
239
+ stack: err.stack,
240
+ to: toState,
241
+ from: fromState,
242
+ },
243
+ );
244
+ }
245
+
246
+ groups.close();
247
+ transitionStartTime = null;
248
+ },
249
+
250
+ teardown() {
251
+ groups.close();
252
+ transitionStartTime = null;
253
+ },
254
+ };
255
+ };
256
+ }
257
+
258
+ /**
259
+ * Default logger-plugin instance with standard configuration.
260
+ * Provided for backward compatibility with existing code.
261
+ *
262
+ * @example
263
+ * // Use default configuration
264
+ * router.usePlugin(loggerPlugin);
265
+ */
266
+ export const loggerPlugin = loggerPluginFactory();
package/src/types.ts ADDED
@@ -0,0 +1,67 @@
1
+ // packages/logger-plugin/modules/types.ts
2
+
3
+ /**
4
+ * Logging level for router events.
5
+ * Controls which events are logged to the console.
6
+ */
7
+ export type LogLevel = "all" | "transitions" | "errors" | "none";
8
+
9
+ /**
10
+ * Configuration options for the logger-plugin.
11
+ */
12
+ export interface LoggerPluginConfig {
13
+ /**
14
+ * Use Performance API to create marks and measures.
15
+ * Enables integration with browser DevTools Performance tab.
16
+ *
17
+ * Creates marks:
18
+ * - `router:transition-start:{from}→{to}`
19
+ * - `router:transition-end:{from}→{to}` (success)
20
+ * - `router:transition-cancel:{from}→{to}` (cancelled)
21
+ * - `router:transition-error:{from}→{to}` (error)
22
+ *
23
+ * Creates measures:
24
+ * - `router:transition:{from}→{to}` (success)
25
+ * - `router:transition-cancelled:{from}→{to}` (cancelled)
26
+ * - `router:transition-failed:{from}→{to}` (error)
27
+ *
28
+ * @default false
29
+ */
30
+ usePerformanceMarks?: boolean;
31
+ /**
32
+ * Logging level - controls what router events to log.
33
+ *
34
+ * - 'all': Log all router events (default)
35
+ * - 'transitions': Log only transition-related events
36
+ * - 'errors': Log only transition errors
37
+ * - 'none': Disable all logging
38
+ *
39
+ * @default 'all'
40
+ */
41
+ level?: LogLevel;
42
+
43
+ /**
44
+ * Show execution time in milliseconds for transitions.
45
+ * Helps identify slow route changes.
46
+ *
47
+ * @default true
48
+ */
49
+ showTiming?: boolean;
50
+
51
+ /**
52
+ * Show diff of changed route parameters between transitions.
53
+ * Only applies when navigating within the same route.
54
+ * Helps identify which parameters changed during navigation.
55
+ *
56
+ * @default false
57
+ */
58
+ showParamsDiff?: boolean;
59
+
60
+ /**
61
+ * Custom context name for console.
62
+ * Useful when running multiple routers.
63
+ *
64
+ * @default 'logger-plugin'
65
+ */
66
+ context?: string;
67
+ }