@crimsonsunset/jsg-logger 1.8.1 → 1.8.3

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/CHANGELOG.md CHANGED
@@ -5,6 +5,35 @@ All notable changes to the JSG Logger project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.8.3] - 2026-03-28 🔌 **addTransport() API**
9
+
10
+ ### Added
11
+ - **`JSGLogger.addTransport(transport)`** — new static method to register a transport on the running singleton without reinitializing. Bypasses the reinit guard entirely. Idempotent (calling with the same instance twice is a no-op). Also exposed on the default export as `logger.addTransport()`.
12
+
13
+ ### Fixed
14
+ - **Transport registration after module-level init** — `jsg-logger` initializes itself with default config at module evaluation time (line 1042). This caused the reinit guard introduced in 1.8.2 to silently drop transports passed to `getInstanceSync(options)` in e.g. Next.js `instrumentation.ts`, because `initialized` was already `true`. `addTransport()` is the correct API for post-init transport registration — it pushes directly to `_instance.transports` and is unaffected by the reinit guard.
15
+
16
+ ### Migration
17
+ Replace `getInstanceSync({ ...config, transports: [myTransport] })` in post-init contexts (e.g. Next.js `register()`) with:
18
+ ```ts
19
+ JSGLogger.addTransport(myTransport);
20
+ ```
21
+
22
+ ## [1.8.2] - 2026-03-27 🛡️ **Reinit Guard + configure() API**
23
+
24
+ ### Added
25
+ - **`JSGLogger.configure(partialConfig)`** — new static method for post-init config updates. Merges partial config into the running instance without reinitializing or touching registered transports. Exposed on the default export as `logger.configure()` and on the class as `JSGLogger.configure()`. If called before any initialization has occurred, delegates to `getInstanceSync(partialConfig)`.
26
+
27
+ ### Fixed
28
+ - **Reinit guard in `getInstanceSync()`** — calling `getInstanceSync(options)` on an already-initialized instance now returns the existing singleton instead of reinitializing. Previously this would wipe registered transports (e.g. PostHog transport) on every call. A `console.warn` is emitted to surface the misuse at call sites.
29
+ - **Reinit guard in `getInstance()`** — same protection applied to the async path.
30
+
31
+ ### Changed
32
+ - Removed stale comment "// If options are provided, we need to reinitialize even if global instance exists" — the behavior is now the opposite by design.
33
+
34
+ ### Migration
35
+ No breaking changes. If you were relying on calling `getInstanceSync(options)` multiple times to update config post-init (which was always a footgun), switch to `logger.configure(partialConfig)` instead.
36
+
8
37
  ## [1.7.7] - 2025-11-07 🔧 **DevTools Import Path Fix**
9
38
 
10
39
  ### Fixed
package/index.d.ts CHANGED
@@ -195,6 +195,12 @@ export interface LoggerInstanceType {
195
195
  */
196
196
  getInstanceSync?: (config?: JSGLoggerConfig) => LoggerInstanceType;
197
197
 
198
+ /**
199
+ * Add a transport to the running singleton without reinitializing. Idempotent.
200
+ * @param transport - LogTransport instance to register
201
+ */
202
+ addTransport?: (transport: LogTransport) => void;
203
+
198
204
  /**
199
205
  * Static performance logging utility
200
206
  */
@@ -225,6 +231,21 @@ export interface JSGLogger {
225
231
  */
226
232
  getInstanceSync(config?: JSGLoggerConfig): LoggerInstanceType;
227
233
 
234
+ /**
235
+ * Add a transport to the running singleton without reinitializing.
236
+ * Safe to call even after the singleton was initialized by module-level code or a
237
+ * third-party library — bypasses the reinit guard entirely. Idempotent.
238
+ *
239
+ * @param transport - LogTransport instance to register
240
+ *
241
+ * @example
242
+ * ```ts
243
+ * // instrumentation.ts — runs after module-level init, still registers transport cleanly
244
+ * JSGLogger.addTransport(new PostHogServerTransport(posthogServer, { level: 'warn' }));
245
+ * ```
246
+ */
247
+ addTransport(transport: LogTransport): void;
248
+
228
249
  /**
229
250
  * Static performance logging utility
230
251
  * @param label - Performance label
package/index.js CHANGED
@@ -81,14 +81,17 @@ class JSGLogger {
81
81
  window.__JSG_Logger_Enhanced__ = JSGLogger._enhancedLoggers;
82
82
  }
83
83
  } else if (hasOptions) {
84
- // Instance exists but new options provided - reinitialize
85
- JSGLogger._enhancedLoggers = await JSGLogger._instance.init(options);
86
-
87
- // Make runtime controls available globally in browser for debugging
88
- // (same as getInstanceSync behavior)
89
- if (isBrowser() && typeof window !== 'undefined' && JSGLogger._enhancedLoggers?.controls) {
90
- window.JSG_Logger = JSGLogger._enhancedLoggers.controls;
91
- window.__JSG_Logger_Enhanced__ = JSGLogger._enhancedLoggers;
84
+ if (JSGLogger._instance.initialized) {
85
+ console.warn(
86
+ '[JSGLogger] getInstance() called with options on an already-initialized instance — ' +
87
+ 'options were ignored to preserve registered transports. Use configure() to update settings post-init.'
88
+ );
89
+ } else {
90
+ JSGLogger._enhancedLoggers = await JSGLogger._instance.init(options);
91
+ if (isBrowser() && typeof window !== 'undefined' && JSGLogger._enhancedLoggers?.controls) {
92
+ window.JSG_Logger = JSGLogger._enhancedLoggers.controls;
93
+ window.__JSG_Logger_Enhanced__ = JSGLogger._enhancedLoggers;
94
+ }
92
95
  }
93
96
  }
94
97
 
@@ -104,22 +107,25 @@ class JSGLogger {
104
107
  static getInstanceSync(options = {}) {
105
108
  const hasOptions = options && Object.keys(options).length > 0;
106
109
 
107
- // If options are provided, we need to reinitialize even if global instance exists
108
- // This ensures config changes (like devtools.enabled) are applied
109
110
  if (hasOptions) {
110
- // Reinitialize with new options - this will update the global references
111
+ if (JSGLogger._instance?.initialized) {
112
+ console.warn(
113
+ '[JSGLogger] getInstanceSync() called with options on an already-initialized instance — ' +
114
+ 'options were ignored to preserve registered transports. Use configure() to update settings post-init.'
115
+ );
116
+ return JSGLogger._enhancedLoggers;
117
+ }
118
+
111
119
  if (!JSGLogger._instance) {
112
120
  JSGLogger._instance = new JSGLogger();
113
121
  }
114
122
  JSGLogger._enhancedLoggers = JSGLogger._instance.initSync(options);
115
123
 
116
- // Update global references after reinitialization
117
124
  if (isBrowser() && typeof window !== 'undefined' && JSGLogger._enhancedLoggers?.controls) {
118
125
  window.JSG_Logger = JSGLogger._enhancedLoggers.controls;
119
126
  window.__JSG_Logger_Enhanced__ = JSGLogger._enhancedLoggers;
120
127
  }
121
128
 
122
- // Server equivalent: persist to globalThis so cross-bundle module instances can find it
123
129
  if (!isBrowser()) {
124
130
  globalThis.__JSG_Logger_Enhanced__ = JSGLogger._enhancedLoggers;
125
131
  }
@@ -559,6 +565,9 @@ class JSGLogger {
559
565
  // Expose config manager for runtime configuration
560
566
  configManager: configManager,
561
567
 
568
+ // Post-init config updater — safe to call after transports are registered
569
+ configure: (partialConfig) => JSGLogger.configure(partialConfig),
570
+
562
571
  // Log store for popup/debugging
563
572
  logStore: this.logStore,
564
573
 
@@ -987,6 +996,52 @@ class JSGLogger {
987
996
  }
988
997
  }
989
998
 
999
+ /**
1000
+ * Update logger configuration post-initialization without reinitializing.
1001
+ * Merges partialConfig into the current config without touching registered transports.
1002
+ * Transports can only be registered at init time — this method cannot add or remove them.
1003
+ * If called before any initialization has occurred, delegates to getInstanceSync(partialConfig).
1004
+ * @param {Object} partialConfig - Partial config to merge into the current config
1005
+ * @returns {Object} Enhanced logger exports
1006
+ */
1007
+ static configure(partialConfig = {}) {
1008
+ if (!JSGLogger._instance?.initialized) {
1009
+ return JSGLogger.getInstanceSync(partialConfig);
1010
+ }
1011
+
1012
+ const currentTransports = JSGLogger._instance.transports;
1013
+ const normalized = configManager._normalizeConfigStructure(partialConfig);
1014
+ configManager.config = configManager.mergeConfigs(configManager.config, normalized);
1015
+ configManager.config.transports = currentTransports;
1016
+ JSGLogger._instance.transports = currentTransports;
1017
+ JSGLogger._instance.refreshLoggers();
1018
+
1019
+ return JSGLogger._enhancedLoggers;
1020
+ }
1021
+
1022
+ /**
1023
+ * Add a transport to the running singleton without reinitializing.
1024
+ * Safe to call even if the singleton was already initialized by module-level code
1025
+ * or a third-party library — bypasses the reinit guard entirely.
1026
+ * Idempotent: calling with the same transport instance twice is a no-op.
1027
+ * @param {Object} transport - LogTransport instance to register
1028
+ * @returns {void}
1029
+ */
1030
+ static addTransport(transport) {
1031
+ if (!transport || typeof transport.send !== 'function') {
1032
+ metaWarn('[JSGLogger] addTransport() received an invalid transport — must have a send() method');
1033
+ return;
1034
+ }
1035
+ if (!JSGLogger._instance) {
1036
+ metaWarn('[JSGLogger] addTransport() called before any initialization — initializing with defaults first');
1037
+ JSGLogger._instance = new JSGLogger();
1038
+ JSGLogger._enhancedLoggers = JSGLogger._instance.initSync({});
1039
+ }
1040
+ if (!JSGLogger._instance.transports.includes(transport)) {
1041
+ JSGLogger._instance.transports.push(transport);
1042
+ }
1043
+ }
1044
+
990
1045
  /**
991
1046
  * Get singleton controls without triggering initialization
992
1047
  * Checks window.JSG_Logger first to ensure singleton works across separate bundles
@@ -1019,6 +1074,8 @@ if (isBrowser() && typeof window !== 'undefined') {
1019
1074
  // Add static methods to the enhanced loggers for convenience
1020
1075
  enhancedLoggers.getInstance = JSGLogger.getInstance;
1021
1076
  enhancedLoggers.getInstanceSync = JSGLogger.getInstanceSync;
1077
+ enhancedLoggers.configure = JSGLogger.configure.bind(JSGLogger);
1078
+ enhancedLoggers.addTransport = JSGLogger.addTransport.bind(JSGLogger);
1022
1079
  enhancedLoggers.logPerformance = JSGLogger.logPerformance;
1023
1080
  enhancedLoggers.JSGLogger = JSGLogger;
1024
1081
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crimsonsunset/jsg-logger",
3
- "version": "1.8.1",
3
+ "version": "1.8.3",
4
4
  "type": "module",
5
5
  "description": "Multi-environment logger with smart detection, file-level overrides, and beautiful console formatting. Test it live: https://logger.joesangiorgio.com/",
6
6
  "main": "index.js",