@objectstack/core 15.1.1 → 16.0.0-rc.0

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/dist/logger.cjs CHANGED
@@ -41,8 +41,34 @@ var LEVEL_COLORS = {
41
41
  silent: ""
42
42
  };
43
43
  var RESET = "\x1B[0m";
44
+ function colorEnabled(stream) {
45
+ if (typeof process !== "undefined") {
46
+ const noColor = process.env?.NO_COLOR;
47
+ if (noColor !== void 0 && noColor !== "") return false;
48
+ }
49
+ return Boolean(stream?.isTTY);
50
+ }
51
+ function loadNodeBuiltin(id) {
52
+ if (typeof process === "undefined") return void 0;
53
+ const getBuiltinModule = process.getBuiltinModule;
54
+ if (typeof getBuiltinModule === "function") {
55
+ try {
56
+ return getBuiltinModule.call(process, `node:${id}`);
57
+ } catch {
58
+ return void 0;
59
+ }
60
+ }
61
+ try {
62
+ return require(id);
63
+ } catch {
64
+ return void 0;
65
+ }
66
+ }
44
67
  var ObjectLogger = class _ObjectLogger {
45
68
  constructor(config = {}, bindings = {}) {
69
+ /** Only the logger that opened the stream may close it — children share it. */
70
+ this.ownsFileStream = false;
71
+ this.fileLoggingDisabled = false;
46
72
  this.config = {
47
73
  name: config.name,
48
74
  level: config.level ?? "info",
@@ -58,12 +84,41 @@ var ObjectLogger = class _ObjectLogger {
58
84
  }
59
85
  }
60
86
  openFileStream(path) {
87
+ const fs = loadNodeBuiltin("fs");
88
+ const nodePath = loadNodeBuiltin("path");
89
+ if (!fs || !nodePath) {
90
+ this.disableFileLogging(path, "no filesystem access in this runtime");
91
+ return;
92
+ }
61
93
  try {
62
- const fs = require("fs");
63
- const dir = require("path").dirname(path);
64
- fs.mkdirSync(dir, { recursive: true });
65
- this.fileStream = fs.createWriteStream(path, { flags: "a" });
66
- } catch {
94
+ fs.mkdirSync(nodePath.dirname(path), { recursive: true });
95
+ const stream = fs.createWriteStream(path, { flags: "a" });
96
+ stream.on("error", (err) => this.disableFileLogging(path, err.message));
97
+ this.fileStream = stream;
98
+ this.ownsFileStream = true;
99
+ } catch (err) {
100
+ this.disableFileLogging(path, err.message);
101
+ }
102
+ }
103
+ /**
104
+ * Report — once — that an explicitly configured `file` destination is not
105
+ * being written, and stop trying.
106
+ *
107
+ * Deliberately not routed through `write()`: this says the logger cannot
108
+ * honour its own config, so `level` must not filter it. The bare `catch {}`
109
+ * this replaces is exactly how #3110 stayed hidden.
110
+ */
111
+ disableFileLogging(path, reason) {
112
+ this.fileStream = void 0;
113
+ this.ownsFileStream = false;
114
+ if (this.fileLoggingDisabled) return;
115
+ this.fileLoggingDisabled = true;
116
+ const label = this.config.name ? `[${this.config.name}] ` : "";
117
+ const notice = `${label}logger: file logging disabled \u2014 cannot write to ${path}: ${reason}`;
118
+ if (typeof process !== "undefined" && process.stderr) {
119
+ process.stderr.write(notice + "\n");
120
+ } else if (typeof console !== "undefined") {
121
+ console.warn(notice);
67
122
  }
68
123
  }
69
124
  isEnabled(level) {
@@ -91,9 +146,13 @@ var ObjectLogger = class _ObjectLogger {
91
146
  });
92
147
  const hasContext = Object.keys(context).length > 0;
93
148
  const ts = (/* @__PURE__ */ new Date()).toISOString();
149
+ const isErrorLevel = level === "error" || level === "fatal";
150
+ const proc = typeof process !== "undefined" ? process : void 0;
151
+ const stream = proc ? isErrorLevel ? proc.stderr : proc.stdout : void 0;
94
152
  let line;
153
+ let plainLine;
95
154
  if (this.config.format === "json") {
96
- line = JSON.stringify({
155
+ line = plainLine = JSON.stringify({
97
156
  time: ts,
98
157
  level,
99
158
  ...this.config.name ? { name: this.config.name } : {},
@@ -103,26 +162,24 @@ var ObjectLogger = class _ObjectLogger {
103
162
  } else if (this.config.format === "text") {
104
163
  const parts = [ts, level.toUpperCase(), message];
105
164
  if (hasContext) parts.push(JSON.stringify(context));
106
- line = parts.join(" | ");
165
+ line = plainLine = parts.join(" | ");
107
166
  } else {
108
- const color = LEVEL_COLORS[level] || "";
109
167
  const label = this.config.name ? `[${this.config.name}] ` : "";
110
- line = `${color}${ts} ${level.toUpperCase()}${RESET} ${label}${message}`;
111
- if (hasContext) line += ` ${JSON.stringify(context)}`;
168
+ const head = `${ts} ${level.toUpperCase()}`;
169
+ let tail = ` ${label}${message}`;
170
+ if (hasContext) tail += ` ${JSON.stringify(context)}`;
171
+ plainLine = head + tail;
172
+ const color = LEVEL_COLORS[level] || "";
173
+ line = color && colorEnabled(stream) ? `${color}${head}${RESET}${tail}` : plainLine;
112
174
  }
113
- const out = line + "\n";
114
- if (typeof process !== "undefined" && process.stderr) {
115
- if (level === "error" || level === "fatal") {
116
- process.stderr.write(out);
117
- } else {
118
- process.stdout?.write(out);
119
- }
175
+ if (stream) {
176
+ stream.write(line + "\n");
120
177
  } else if (typeof console !== "undefined") {
121
178
  const fn = level === "error" || level === "fatal" ? console.error : level === "warn" ? console.warn : level === "debug" ? console.debug : console.log;
122
179
  fn(line);
123
180
  }
124
181
  if (this.fileStream) {
125
- this.fileStream.write(out);
182
+ this.fileStream.write(plainLine + "\n");
126
183
  }
127
184
  }
128
185
  debug(message, meta) {
@@ -152,7 +209,8 @@ var ObjectLogger = class _ObjectLogger {
152
209
  this.info(message, args.length > 0 ? { args } : void 0);
153
210
  }
154
211
  child(context) {
155
- const child = new _ObjectLogger(this.config, { ...this.bindings, ...context });
212
+ const child = new _ObjectLogger({ ...this.config, file: void 0 }, { ...this.bindings, ...context });
213
+ child.config.file = this.config.file;
156
214
  child.fileStream = this.fileStream;
157
215
  return child;
158
216
  }
@@ -160,10 +218,11 @@ var ObjectLogger = class _ObjectLogger {
160
218
  return this.child({ traceId, spanId });
161
219
  }
162
220
  async destroy() {
163
- if (this.fileStream) {
164
- await new Promise((resolve) => this.fileStream.end(resolve));
165
- this.fileStream = void 0;
166
- }
221
+ const stream = this.fileStream;
222
+ this.fileStream = void 0;
223
+ if (!stream || !this.ownsFileStream) return;
224
+ this.ownsFileStream = false;
225
+ await new Promise((resolve) => stream.end(resolve));
167
226
  }
168
227
  };
169
228
  function createLogger(config) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/logger.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { LoggerConfig, LogLevel } from '@objectstack/spec/system';\nimport type { Logger } from '@objectstack/spec/contracts';\n\n// Re-export the contract type so consumers can do\n// `import type { Logger } from '@objectstack/core/logger'` without also\n// pulling `@objectstack/spec` into their bundle graph manually.\nexport type { Logger };\n\nconst LEVEL_ORDER: Record<LogLevel, number> = {\n debug: 0,\n info: 1,\n warn: 2,\n error: 3,\n fatal: 4,\n silent: 5,\n};\n\nconst LEVEL_COLORS: Record<LogLevel, string> = {\n debug: '\\x1b[36m',\n info: '\\x1b[32m',\n warn: '\\x1b[33m',\n error: '\\x1b[31m',\n fatal: '\\x1b[35m',\n silent: '',\n};\n\nconst RESET = '\\x1b[0m';\n\nexport class ObjectLogger implements Logger {\n private config: Required<Omit<LoggerConfig, 'file' | 'rotation' | 'name'>> & {\n file?: string;\n rotation?: { maxSize: string; maxFiles: number };\n name?: string;\n };\n private bindings: Record<string, any>;\n private fileStream?: any;\n\n constructor(config: Partial<LoggerConfig> = {}, bindings: Record<string, any> = {}) {\n this.config = {\n name: config.name,\n level: config.level ?? 'info',\n format: config.format ?? 'pretty',\n redact: config.redact ?? ['password', 'token', 'secret', 'key'],\n sourceLocation: config.sourceLocation ?? false,\n file: config.file,\n rotation: config.rotation ?? { maxSize: '10m', maxFiles: 5 },\n };\n this.bindings = bindings;\n\n if (this.config.file && typeof process !== 'undefined') {\n this.openFileStream(this.config.file);\n }\n }\n\n private openFileStream(path: string) {\n try {\n // Lazy require to avoid bundling issues\n const fs = require('fs');\n const dir = require('path').dirname(path);\n fs.mkdirSync(dir, { recursive: true });\n this.fileStream = fs.createWriteStream(path, { flags: 'a' });\n } catch {\n // ignore — file logging is optional\n }\n }\n\n private isEnabled(level: LogLevel): boolean {\n return LEVEL_ORDER[level] >= LEVEL_ORDER[this.config.level];\n }\n\n private redactSensitive(obj: any): any {\n if (!obj || typeof obj !== 'object') return obj;\n const redacted = Array.isArray(obj) ? [...obj] : { ...obj };\n for (const key in redacted) {\n const lower = key.toLowerCase();\n if (this.config.redact.some((p: string) => lower.includes(p.toLowerCase()))) {\n redacted[key] = '***REDACTED***';\n } else if (typeof redacted[key] === 'object' && redacted[key] !== null) {\n redacted[key] = this.redactSensitive(redacted[key]);\n }\n }\n return redacted;\n }\n\n private write(level: LogLevel, message: string, meta?: Record<string, any>, error?: Error) {\n if (!this.isEnabled(level)) return;\n\n const context = this.redactSensitive({\n ...this.bindings,\n ...meta,\n ...(error ? { error: { message: error.message, stack: error.stack } } : {}),\n });\n\n const hasContext = Object.keys(context).length > 0;\n const ts = new Date().toISOString();\n\n let line: string;\n\n if (this.config.format === 'json') {\n line = JSON.stringify({\n time: ts,\n level,\n ...(this.config.name ? { name: this.config.name } : {}),\n msg: message,\n ...context,\n });\n } else if (this.config.format === 'text') {\n const parts = [ts, level.toUpperCase(), message];\n if (hasContext) parts.push(JSON.stringify(context));\n line = parts.join(' | ');\n } else {\n // pretty\n const color = LEVEL_COLORS[level] || '';\n const label = this.config.name ? `[${this.config.name}] ` : '';\n line = `${color}${ts} ${level.toUpperCase()}${RESET} ${label}${message}`;\n if (hasContext) line += ` ${JSON.stringify(context)}`;\n }\n\n const out = line + '\\n';\n\n // Browser-safe output: prefer process streams when available, otherwise\n // fall back to console. The previous unguarded `process.stderr?.write`\n // throws `ReferenceError: process is not defined` in browsers because\n // `process` itself is the missing global, not just its `stderr` field.\n if (typeof process !== 'undefined' && (process as any).stderr) {\n if (level === 'error' || level === 'fatal') {\n (process as any).stderr.write(out);\n } else {\n (process as any).stdout?.write(out);\n }\n } else if (typeof console !== 'undefined') {\n const fn =\n level === 'error' || level === 'fatal' ? console.error\n : level === 'warn' ? console.warn\n : level === 'debug' ? console.debug\n : console.log;\n fn(line);\n }\n\n if (this.fileStream) {\n this.fileStream.write(out);\n }\n }\n\n debug(message: string, meta?: Record<string, any>): void {\n this.write('debug', message, meta);\n }\n\n info(message: string, meta?: Record<string, any>): void {\n this.write('info', message, meta);\n }\n\n warn(message: string, meta?: Record<string, any>): void {\n this.write('warn', message, meta);\n }\n\n error(message: string, errorOrMeta?: Error | Record<string, any>, meta?: Record<string, any>): void {\n if (errorOrMeta instanceof Error) {\n this.write('error', message, meta, errorOrMeta);\n } else {\n this.write('error', message, errorOrMeta);\n }\n }\n\n fatal(message: string, errorOrMeta?: Error | Record<string, any>, meta?: Record<string, any>): void {\n if (errorOrMeta instanceof Error) {\n this.write('fatal', message, meta, errorOrMeta);\n } else {\n this.write('fatal', message, errorOrMeta);\n }\n }\n\n log(message: string, ...args: any[]): void {\n this.info(message, args.length > 0 ? { args } : undefined);\n }\n\n child(context: Record<string, any>): ObjectLogger {\n const child = new ObjectLogger(this.config, { ...this.bindings, ...context });\n // Share the file stream — no double-open\n child.fileStream = this.fileStream;\n return child;\n }\n\n withTrace(traceId: string, spanId?: string): ObjectLogger {\n return this.child({ traceId, spanId });\n }\n\n async destroy(): Promise<void> {\n if (this.fileStream) {\n await new Promise<void>((resolve) => this.fileStream.end(resolve));\n this.fileStream = undefined;\n }\n }\n}\n\nexport function createLogger(config?: Partial<LoggerConfig>): ObjectLogger {\n return new ObjectLogger(config);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/logger.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { LoggerConfig, LogLevel } from '@objectstack/spec/system';\nimport type { Logger } from '@objectstack/spec/contracts';\n\n// Re-export the contract type so consumers can do\n// `import type { Logger } from '@objectstack/core/logger'` without also\n// pulling `@objectstack/spec` into their bundle graph manually.\nexport type { Logger };\n\nconst LEVEL_ORDER: Record<LogLevel, number> = {\n debug: 0,\n info: 1,\n warn: 2,\n error: 3,\n fatal: 4,\n silent: 5,\n};\n\nconst LEVEL_COLORS: Record<LogLevel, string> = {\n debug: '\\x1b[36m',\n info: '\\x1b[32m',\n warn: '\\x1b[33m',\n error: '\\x1b[31m',\n fatal: '\\x1b[35m',\n silent: '',\n};\n\nconst RESET = '\\x1b[0m';\n\n/**\n * Whether ANSI color may be written to the given stream.\n *\n * Follows the https://no-color.org convention: a non-empty `NO_COLOR` env var\n * disables color regardless of TTY, and non-TTY destinations (pipes, CI logs,\n * redirected output) always get plain text so plain-text log scanners see\n * uncolored level tags. Browser bundles have no `process`/TTY → plain text.\n */\nfunction colorEnabled(stream: { isTTY?: boolean } | undefined): boolean {\n if (typeof process !== 'undefined') {\n const noColor = (process as any).env?.NO_COLOR;\n if (noColor !== undefined && noColor !== '') return false;\n }\n return Boolean(stream?.isTTY);\n}\n\n/**\n * Resolve a Node builtin without putting it in this module's import graph.\n *\n * This entry is deliberately browser-safe — `@objectstack/client` bundles it —\n * so `fs`/`path` must never be imported statically. A lazy `require()` used to\n * meet that bar, but esbuild rewrites it to the `__require` shim in the ESM\n * output, which throws `Dynamic require of \"fs\" is not supported`. Every Node\n * ESM consumer (`os serve`, `os dev`) therefore lost file logging (#3110).\n * `process.getBuiltinModule` is a plain method call — opaque to bundlers — and\n * works in both module systems.\n */\nfunction loadNodeBuiltin<T>(id: string): T | undefined {\n if (typeof process === 'undefined') return undefined;\n\n const getBuiltinModule = (process as { getBuiltinModule?: (id: string) => unknown }).getBuiltinModule;\n if (typeof getBuiltinModule === 'function') {\n try {\n return getBuiltinModule.call(process, `node:${id}`) as T;\n } catch {\n return undefined;\n }\n }\n\n // Node < 20.16 / < 22.3 predates `getBuiltinModule`. Real `require` still\n // resolves in the CJS build; in the ESM build this is the shim that throws,\n // which the caller now reports rather than swallows.\n try {\n return require(id) as T;\n } catch {\n return undefined;\n }\n}\n\nexport class ObjectLogger implements Logger {\n private config: Required<Omit<LoggerConfig, 'file' | 'rotation' | 'name'>> & {\n file?: string;\n rotation?: { maxSize: string; maxFiles: number };\n name?: string;\n };\n private bindings: Record<string, any>;\n private fileStream?: any;\n /** Only the logger that opened the stream may close it — children share it. */\n private ownsFileStream = false;\n private fileLoggingDisabled = false;\n\n constructor(config: Partial<LoggerConfig> = {}, bindings: Record<string, any> = {}) {\n this.config = {\n name: config.name,\n level: config.level ?? 'info',\n format: config.format ?? 'pretty',\n redact: config.redact ?? ['password', 'token', 'secret', 'key'],\n sourceLocation: config.sourceLocation ?? false,\n file: config.file,\n rotation: config.rotation ?? { maxSize: '10m', maxFiles: 5 },\n };\n this.bindings = bindings;\n\n if (this.config.file && typeof process !== 'undefined') {\n this.openFileStream(this.config.file);\n }\n }\n\n private openFileStream(path: string) {\n const fs = loadNodeBuiltin<typeof import('node:fs')>('fs');\n const nodePath = loadNodeBuiltin<typeof import('node:path')>('path');\n if (!fs || !nodePath) {\n this.disableFileLogging(path, 'no filesystem access in this runtime');\n return;\n }\n\n try {\n fs.mkdirSync(nodePath.dirname(path), { recursive: true });\n const stream = fs.createWriteStream(path, { flags: 'a' });\n // `createWriteStream` reports open failures (EACCES, EISDIR, …)\n // asynchronously. An 'error' event with no listener is fatal to the\n // process, so file logging must degrade here rather than take the\n // host down.\n stream.on('error', (err: Error) => this.disableFileLogging(path, err.message));\n this.fileStream = stream;\n this.ownsFileStream = true;\n } catch (err) {\n this.disableFileLogging(path, (err as Error).message);\n }\n }\n\n /**\n * Report — once — that an explicitly configured `file` destination is not\n * being written, and stop trying.\n *\n * Deliberately not routed through `write()`: this says the logger cannot\n * honour its own config, so `level` must not filter it. The bare `catch {}`\n * this replaces is exactly how #3110 stayed hidden.\n */\n private disableFileLogging(path: string, reason: string) {\n this.fileStream = undefined;\n this.ownsFileStream = false;\n if (this.fileLoggingDisabled) return;\n this.fileLoggingDisabled = true;\n\n const label = this.config.name ? `[${this.config.name}] ` : '';\n const notice = `${label}logger: file logging disabled — cannot write to ${path}: ${reason}`;\n if (typeof process !== 'undefined' && (process as any).stderr) {\n (process as any).stderr.write(notice + '\\n');\n } else if (typeof console !== 'undefined') {\n console.warn(notice);\n }\n }\n\n private isEnabled(level: LogLevel): boolean {\n return LEVEL_ORDER[level] >= LEVEL_ORDER[this.config.level];\n }\n\n private redactSensitive(obj: any): any {\n if (!obj || typeof obj !== 'object') return obj;\n const redacted = Array.isArray(obj) ? [...obj] : { ...obj };\n for (const key in redacted) {\n const lower = key.toLowerCase();\n if (this.config.redact.some((p: string) => lower.includes(p.toLowerCase()))) {\n redacted[key] = '***REDACTED***';\n } else if (typeof redacted[key] === 'object' && redacted[key] !== null) {\n redacted[key] = this.redactSensitive(redacted[key]);\n }\n }\n return redacted;\n }\n\n private write(level: LogLevel, message: string, meta?: Record<string, any>, error?: Error) {\n if (!this.isEnabled(level)) return;\n\n const context = this.redactSensitive({\n ...this.bindings,\n ...meta,\n ...(error ? { error: { message: error.message, stack: error.stack } } : {}),\n });\n\n const hasContext = Object.keys(context).length > 0;\n const ts = new Date().toISOString();\n\n const isErrorLevel = level === 'error' || level === 'fatal';\n const proc = typeof process !== 'undefined' ? (process as any) : undefined;\n const stream = proc ? (isErrorLevel ? proc.stderr : proc.stdout) : undefined;\n\n let line: string; // console output — may carry ANSI color\n let plainLine: string; // file output — never colored\n\n if (this.config.format === 'json') {\n line = plainLine = JSON.stringify({\n time: ts,\n level,\n ...(this.config.name ? { name: this.config.name } : {}),\n msg: message,\n ...context,\n });\n } else if (this.config.format === 'text') {\n const parts = [ts, level.toUpperCase(), message];\n if (hasContext) parts.push(JSON.stringify(context));\n line = plainLine = parts.join(' | ');\n } else {\n // pretty\n const label = this.config.name ? `[${this.config.name}] ` : '';\n const head = `${ts} ${level.toUpperCase()}`;\n let tail = ` ${label}${message}`;\n if (hasContext) tail += ` ${JSON.stringify(context)}`;\n plainLine = head + tail;\n const color = LEVEL_COLORS[level] || '';\n line = color && colorEnabled(stream) ? `${color}${head}${RESET}${tail}` : plainLine;\n }\n\n // Browser-safe output: prefer process streams when available, otherwise\n // fall back to console. `process` may be missing entirely (browsers) or\n // present without stdio streams (bundler shims) — both fall through to\n // console. The previous unguarded `process.stderr?.write` threw\n // `ReferenceError: process is not defined` in browsers because\n // `process` itself is the missing global, not just its `stderr` field.\n if (stream) {\n stream.write(line + '\\n');\n } else if (typeof console !== 'undefined') {\n const fn =\n level === 'error' || level === 'fatal' ? console.error\n : level === 'warn' ? console.warn\n : level === 'debug' ? console.debug\n : console.log;\n fn(line);\n }\n\n if (this.fileStream) {\n this.fileStream.write(plainLine + '\\n');\n }\n }\n\n debug(message: string, meta?: Record<string, any>): void {\n this.write('debug', message, meta);\n }\n\n info(message: string, meta?: Record<string, any>): void {\n this.write('info', message, meta);\n }\n\n warn(message: string, meta?: Record<string, any>): void {\n this.write('warn', message, meta);\n }\n\n error(message: string, errorOrMeta?: Error | Record<string, any>, meta?: Record<string, any>): void {\n if (errorOrMeta instanceof Error) {\n this.write('error', message, meta, errorOrMeta);\n } else {\n this.write('error', message, errorOrMeta);\n }\n }\n\n fatal(message: string, errorOrMeta?: Error | Record<string, any>, meta?: Record<string, any>): void {\n if (errorOrMeta instanceof Error) {\n this.write('fatal', message, meta, errorOrMeta);\n } else {\n this.write('fatal', message, errorOrMeta);\n }\n }\n\n log(message: string, ...args: any[]): void {\n this.info(message, args.length > 0 ? { args } : undefined);\n }\n\n child(context: Record<string, any>): ObjectLogger {\n // Construct without `file`, then share the parent's stream: the\n // constructor opens eagerly, so passing `file` through would open a\n // second stream per child and immediately orphan it. That leak was\n // unreachable while #3110 kept the ESM open path dead.\n const child = new ObjectLogger({ ...this.config, file: undefined }, { ...this.bindings, ...context });\n child.config.file = this.config.file;\n child.fileStream = this.fileStream;\n return child;\n }\n\n withTrace(traceId: string, spanId?: string): ObjectLogger {\n return this.child({ traceId, spanId });\n }\n\n async destroy(): Promise<void> {\n const stream = this.fileStream;\n this.fileStream = undefined;\n // Children share the opener's stream; if they closed it too, one child's\n // teardown would end file logging for the parent and every sibling,\n // whose writes then land on a closed stream and only trip the 'error'\n // handler above.\n if (!stream || !this.ownsFileStream) return;\n this.ownsFileStream = false;\n await new Promise<void>((resolve) => stream.end(resolve));\n }\n}\n\nexport function createLogger(config?: Partial<LoggerConfig>): ObjectLogger {\n return new ObjectLogger(config);\n}\n"],"mappings":"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","names":[]}
package/dist/logger.d.cts CHANGED
@@ -6,8 +6,20 @@ declare class ObjectLogger implements Logger {
6
6
  private config;
7
7
  private bindings;
8
8
  private fileStream?;
9
+ /** Only the logger that opened the stream may close it — children share it. */
10
+ private ownsFileStream;
11
+ private fileLoggingDisabled;
9
12
  constructor(config?: Partial<LoggerConfig>, bindings?: Record<string, any>);
10
13
  private openFileStream;
14
+ /**
15
+ * Report — once — that an explicitly configured `file` destination is not
16
+ * being written, and stop trying.
17
+ *
18
+ * Deliberately not routed through `write()`: this says the logger cannot
19
+ * honour its own config, so `level` must not filter it. The bare `catch {}`
20
+ * this replaces is exactly how #3110 stayed hidden.
21
+ */
22
+ private disableFileLogging;
11
23
  private isEnabled;
12
24
  private redactSensitive;
13
25
  private write;
package/dist/logger.d.ts CHANGED
@@ -6,8 +6,20 @@ declare class ObjectLogger implements Logger {
6
6
  private config;
7
7
  private bindings;
8
8
  private fileStream?;
9
+ /** Only the logger that opened the stream may close it — children share it. */
10
+ private ownsFileStream;
11
+ private fileLoggingDisabled;
9
12
  constructor(config?: Partial<LoggerConfig>, bindings?: Record<string, any>);
10
13
  private openFileStream;
14
+ /**
15
+ * Report — once — that an explicitly configured `file` destination is not
16
+ * being written, and stop trying.
17
+ *
18
+ * Deliberately not routed through `write()`: this says the logger cannot
19
+ * honour its own config, so `level` must not filter it. The bare `catch {}`
20
+ * this replaces is exactly how #3110 stayed hidden.
21
+ */
22
+ private disableFileLogging;
11
23
  private isEnabled;
12
24
  private redactSensitive;
13
25
  private write;
package/dist/logger.js CHANGED
@@ -23,8 +23,34 @@ var LEVEL_COLORS = {
23
23
  silent: ""
24
24
  };
25
25
  var RESET = "\x1B[0m";
26
+ function colorEnabled(stream) {
27
+ if (typeof process !== "undefined") {
28
+ const noColor = process.env?.NO_COLOR;
29
+ if (noColor !== void 0 && noColor !== "") return false;
30
+ }
31
+ return Boolean(stream?.isTTY);
32
+ }
33
+ function loadNodeBuiltin(id) {
34
+ if (typeof process === "undefined") return void 0;
35
+ const getBuiltinModule = process.getBuiltinModule;
36
+ if (typeof getBuiltinModule === "function") {
37
+ try {
38
+ return getBuiltinModule.call(process, `node:${id}`);
39
+ } catch {
40
+ return void 0;
41
+ }
42
+ }
43
+ try {
44
+ return __require(id);
45
+ } catch {
46
+ return void 0;
47
+ }
48
+ }
26
49
  var ObjectLogger = class _ObjectLogger {
27
50
  constructor(config = {}, bindings = {}) {
51
+ /** Only the logger that opened the stream may close it — children share it. */
52
+ this.ownsFileStream = false;
53
+ this.fileLoggingDisabled = false;
28
54
  this.config = {
29
55
  name: config.name,
30
56
  level: config.level ?? "info",
@@ -40,12 +66,41 @@ var ObjectLogger = class _ObjectLogger {
40
66
  }
41
67
  }
42
68
  openFileStream(path) {
69
+ const fs = loadNodeBuiltin("fs");
70
+ const nodePath = loadNodeBuiltin("path");
71
+ if (!fs || !nodePath) {
72
+ this.disableFileLogging(path, "no filesystem access in this runtime");
73
+ return;
74
+ }
43
75
  try {
44
- const fs = __require("fs");
45
- const dir = __require("path").dirname(path);
46
- fs.mkdirSync(dir, { recursive: true });
47
- this.fileStream = fs.createWriteStream(path, { flags: "a" });
48
- } catch {
76
+ fs.mkdirSync(nodePath.dirname(path), { recursive: true });
77
+ const stream = fs.createWriteStream(path, { flags: "a" });
78
+ stream.on("error", (err) => this.disableFileLogging(path, err.message));
79
+ this.fileStream = stream;
80
+ this.ownsFileStream = true;
81
+ } catch (err) {
82
+ this.disableFileLogging(path, err.message);
83
+ }
84
+ }
85
+ /**
86
+ * Report — once — that an explicitly configured `file` destination is not
87
+ * being written, and stop trying.
88
+ *
89
+ * Deliberately not routed through `write()`: this says the logger cannot
90
+ * honour its own config, so `level` must not filter it. The bare `catch {}`
91
+ * this replaces is exactly how #3110 stayed hidden.
92
+ */
93
+ disableFileLogging(path, reason) {
94
+ this.fileStream = void 0;
95
+ this.ownsFileStream = false;
96
+ if (this.fileLoggingDisabled) return;
97
+ this.fileLoggingDisabled = true;
98
+ const label = this.config.name ? `[${this.config.name}] ` : "";
99
+ const notice = `${label}logger: file logging disabled \u2014 cannot write to ${path}: ${reason}`;
100
+ if (typeof process !== "undefined" && process.stderr) {
101
+ process.stderr.write(notice + "\n");
102
+ } else if (typeof console !== "undefined") {
103
+ console.warn(notice);
49
104
  }
50
105
  }
51
106
  isEnabled(level) {
@@ -73,9 +128,13 @@ var ObjectLogger = class _ObjectLogger {
73
128
  });
74
129
  const hasContext = Object.keys(context).length > 0;
75
130
  const ts = (/* @__PURE__ */ new Date()).toISOString();
131
+ const isErrorLevel = level === "error" || level === "fatal";
132
+ const proc = typeof process !== "undefined" ? process : void 0;
133
+ const stream = proc ? isErrorLevel ? proc.stderr : proc.stdout : void 0;
76
134
  let line;
135
+ let plainLine;
77
136
  if (this.config.format === "json") {
78
- line = JSON.stringify({
137
+ line = plainLine = JSON.stringify({
79
138
  time: ts,
80
139
  level,
81
140
  ...this.config.name ? { name: this.config.name } : {},
@@ -85,26 +144,24 @@ var ObjectLogger = class _ObjectLogger {
85
144
  } else if (this.config.format === "text") {
86
145
  const parts = [ts, level.toUpperCase(), message];
87
146
  if (hasContext) parts.push(JSON.stringify(context));
88
- line = parts.join(" | ");
147
+ line = plainLine = parts.join(" | ");
89
148
  } else {
90
- const color = LEVEL_COLORS[level] || "";
91
149
  const label = this.config.name ? `[${this.config.name}] ` : "";
92
- line = `${color}${ts} ${level.toUpperCase()}${RESET} ${label}${message}`;
93
- if (hasContext) line += ` ${JSON.stringify(context)}`;
150
+ const head = `${ts} ${level.toUpperCase()}`;
151
+ let tail = ` ${label}${message}`;
152
+ if (hasContext) tail += ` ${JSON.stringify(context)}`;
153
+ plainLine = head + tail;
154
+ const color = LEVEL_COLORS[level] || "";
155
+ line = color && colorEnabled(stream) ? `${color}${head}${RESET}${tail}` : plainLine;
94
156
  }
95
- const out = line + "\n";
96
- if (typeof process !== "undefined" && process.stderr) {
97
- if (level === "error" || level === "fatal") {
98
- process.stderr.write(out);
99
- } else {
100
- process.stdout?.write(out);
101
- }
157
+ if (stream) {
158
+ stream.write(line + "\n");
102
159
  } else if (typeof console !== "undefined") {
103
160
  const fn = level === "error" || level === "fatal" ? console.error : level === "warn" ? console.warn : level === "debug" ? console.debug : console.log;
104
161
  fn(line);
105
162
  }
106
163
  if (this.fileStream) {
107
- this.fileStream.write(out);
164
+ this.fileStream.write(plainLine + "\n");
108
165
  }
109
166
  }
110
167
  debug(message, meta) {
@@ -134,7 +191,8 @@ var ObjectLogger = class _ObjectLogger {
134
191
  this.info(message, args.length > 0 ? { args } : void 0);
135
192
  }
136
193
  child(context) {
137
- const child = new _ObjectLogger(this.config, { ...this.bindings, ...context });
194
+ const child = new _ObjectLogger({ ...this.config, file: void 0 }, { ...this.bindings, ...context });
195
+ child.config.file = this.config.file;
138
196
  child.fileStream = this.fileStream;
139
197
  return child;
140
198
  }
@@ -142,10 +200,11 @@ var ObjectLogger = class _ObjectLogger {
142
200
  return this.child({ traceId, spanId });
143
201
  }
144
202
  async destroy() {
145
- if (this.fileStream) {
146
- await new Promise((resolve) => this.fileStream.end(resolve));
147
- this.fileStream = void 0;
148
- }
203
+ const stream = this.fileStream;
204
+ this.fileStream = void 0;
205
+ if (!stream || !this.ownsFileStream) return;
206
+ this.ownsFileStream = false;
207
+ await new Promise((resolve) => stream.end(resolve));
149
208
  }
150
209
  };
151
210
  function createLogger(config) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/logger.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { LoggerConfig, LogLevel } from '@objectstack/spec/system';\nimport type { Logger } from '@objectstack/spec/contracts';\n\n// Re-export the contract type so consumers can do\n// `import type { Logger } from '@objectstack/core/logger'` without also\n// pulling `@objectstack/spec` into their bundle graph manually.\nexport type { Logger };\n\nconst LEVEL_ORDER: Record<LogLevel, number> = {\n debug: 0,\n info: 1,\n warn: 2,\n error: 3,\n fatal: 4,\n silent: 5,\n};\n\nconst LEVEL_COLORS: Record<LogLevel, string> = {\n debug: '\\x1b[36m',\n info: '\\x1b[32m',\n warn: '\\x1b[33m',\n error: '\\x1b[31m',\n fatal: '\\x1b[35m',\n silent: '',\n};\n\nconst RESET = '\\x1b[0m';\n\nexport class ObjectLogger implements Logger {\n private config: Required<Omit<LoggerConfig, 'file' | 'rotation' | 'name'>> & {\n file?: string;\n rotation?: { maxSize: string; maxFiles: number };\n name?: string;\n };\n private bindings: Record<string, any>;\n private fileStream?: any;\n\n constructor(config: Partial<LoggerConfig> = {}, bindings: Record<string, any> = {}) {\n this.config = {\n name: config.name,\n level: config.level ?? 'info',\n format: config.format ?? 'pretty',\n redact: config.redact ?? ['password', 'token', 'secret', 'key'],\n sourceLocation: config.sourceLocation ?? false,\n file: config.file,\n rotation: config.rotation ?? { maxSize: '10m', maxFiles: 5 },\n };\n this.bindings = bindings;\n\n if (this.config.file && typeof process !== 'undefined') {\n this.openFileStream(this.config.file);\n }\n }\n\n private openFileStream(path: string) {\n try {\n // Lazy require to avoid bundling issues\n const fs = require('fs');\n const dir = require('path').dirname(path);\n fs.mkdirSync(dir, { recursive: true });\n this.fileStream = fs.createWriteStream(path, { flags: 'a' });\n } catch {\n // ignore — file logging is optional\n }\n }\n\n private isEnabled(level: LogLevel): boolean {\n return LEVEL_ORDER[level] >= LEVEL_ORDER[this.config.level];\n }\n\n private redactSensitive(obj: any): any {\n if (!obj || typeof obj !== 'object') return obj;\n const redacted = Array.isArray(obj) ? [...obj] : { ...obj };\n for (const key in redacted) {\n const lower = key.toLowerCase();\n if (this.config.redact.some((p: string) => lower.includes(p.toLowerCase()))) {\n redacted[key] = '***REDACTED***';\n } else if (typeof redacted[key] === 'object' && redacted[key] !== null) {\n redacted[key] = this.redactSensitive(redacted[key]);\n }\n }\n return redacted;\n }\n\n private write(level: LogLevel, message: string, meta?: Record<string, any>, error?: Error) {\n if (!this.isEnabled(level)) return;\n\n const context = this.redactSensitive({\n ...this.bindings,\n ...meta,\n ...(error ? { error: { message: error.message, stack: error.stack } } : {}),\n });\n\n const hasContext = Object.keys(context).length > 0;\n const ts = new Date().toISOString();\n\n let line: string;\n\n if (this.config.format === 'json') {\n line = JSON.stringify({\n time: ts,\n level,\n ...(this.config.name ? { name: this.config.name } : {}),\n msg: message,\n ...context,\n });\n } else if (this.config.format === 'text') {\n const parts = [ts, level.toUpperCase(), message];\n if (hasContext) parts.push(JSON.stringify(context));\n line = parts.join(' | ');\n } else {\n // pretty\n const color = LEVEL_COLORS[level] || '';\n const label = this.config.name ? `[${this.config.name}] ` : '';\n line = `${color}${ts} ${level.toUpperCase()}${RESET} ${label}${message}`;\n if (hasContext) line += ` ${JSON.stringify(context)}`;\n }\n\n const out = line + '\\n';\n\n // Browser-safe output: prefer process streams when available, otherwise\n // fall back to console. The previous unguarded `process.stderr?.write`\n // throws `ReferenceError: process is not defined` in browsers because\n // `process` itself is the missing global, not just its `stderr` field.\n if (typeof process !== 'undefined' && (process as any).stderr) {\n if (level === 'error' || level === 'fatal') {\n (process as any).stderr.write(out);\n } else {\n (process as any).stdout?.write(out);\n }\n } else if (typeof console !== 'undefined') {\n const fn =\n level === 'error' || level === 'fatal' ? console.error\n : level === 'warn' ? console.warn\n : level === 'debug' ? console.debug\n : console.log;\n fn(line);\n }\n\n if (this.fileStream) {\n this.fileStream.write(out);\n }\n }\n\n debug(message: string, meta?: Record<string, any>): void {\n this.write('debug', message, meta);\n }\n\n info(message: string, meta?: Record<string, any>): void {\n this.write('info', message, meta);\n }\n\n warn(message: string, meta?: Record<string, any>): void {\n this.write('warn', message, meta);\n }\n\n error(message: string, errorOrMeta?: Error | Record<string, any>, meta?: Record<string, any>): void {\n if (errorOrMeta instanceof Error) {\n this.write('error', message, meta, errorOrMeta);\n } else {\n this.write('error', message, errorOrMeta);\n }\n }\n\n fatal(message: string, errorOrMeta?: Error | Record<string, any>, meta?: Record<string, any>): void {\n if (errorOrMeta instanceof Error) {\n this.write('fatal', message, meta, errorOrMeta);\n } else {\n this.write('fatal', message, errorOrMeta);\n }\n }\n\n log(message: string, ...args: any[]): void {\n this.info(message, args.length > 0 ? { args } : undefined);\n }\n\n child(context: Record<string, any>): ObjectLogger {\n const child = new ObjectLogger(this.config, { ...this.bindings, ...context });\n // Share the file stream — no double-open\n child.fileStream = this.fileStream;\n return child;\n }\n\n withTrace(traceId: string, spanId?: string): ObjectLogger {\n return this.child({ traceId, spanId });\n }\n\n async destroy(): Promise<void> {\n if (this.fileStream) {\n await new Promise<void>((resolve) => this.fileStream.end(resolve));\n this.fileStream = undefined;\n }\n }\n}\n\nexport function createLogger(config?: Partial<LoggerConfig>): ObjectLogger {\n return new ObjectLogger(config);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/logger.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { LoggerConfig, LogLevel } from '@objectstack/spec/system';\nimport type { Logger } from '@objectstack/spec/contracts';\n\n// Re-export the contract type so consumers can do\n// `import type { Logger } from '@objectstack/core/logger'` without also\n// pulling `@objectstack/spec` into their bundle graph manually.\nexport type { Logger };\n\nconst LEVEL_ORDER: Record<LogLevel, number> = {\n debug: 0,\n info: 1,\n warn: 2,\n error: 3,\n fatal: 4,\n silent: 5,\n};\n\nconst LEVEL_COLORS: Record<LogLevel, string> = {\n debug: '\\x1b[36m',\n info: '\\x1b[32m',\n warn: '\\x1b[33m',\n error: '\\x1b[31m',\n fatal: '\\x1b[35m',\n silent: '',\n};\n\nconst RESET = '\\x1b[0m';\n\n/**\n * Whether ANSI color may be written to the given stream.\n *\n * Follows the https://no-color.org convention: a non-empty `NO_COLOR` env var\n * disables color regardless of TTY, and non-TTY destinations (pipes, CI logs,\n * redirected output) always get plain text so plain-text log scanners see\n * uncolored level tags. Browser bundles have no `process`/TTY → plain text.\n */\nfunction colorEnabled(stream: { isTTY?: boolean } | undefined): boolean {\n if (typeof process !== 'undefined') {\n const noColor = (process as any).env?.NO_COLOR;\n if (noColor !== undefined && noColor !== '') return false;\n }\n return Boolean(stream?.isTTY);\n}\n\n/**\n * Resolve a Node builtin without putting it in this module's import graph.\n *\n * This entry is deliberately browser-safe — `@objectstack/client` bundles it —\n * so `fs`/`path` must never be imported statically. A lazy `require()` used to\n * meet that bar, but esbuild rewrites it to the `__require` shim in the ESM\n * output, which throws `Dynamic require of \"fs\" is not supported`. Every Node\n * ESM consumer (`os serve`, `os dev`) therefore lost file logging (#3110).\n * `process.getBuiltinModule` is a plain method call — opaque to bundlers — and\n * works in both module systems.\n */\nfunction loadNodeBuiltin<T>(id: string): T | undefined {\n if (typeof process === 'undefined') return undefined;\n\n const getBuiltinModule = (process as { getBuiltinModule?: (id: string) => unknown }).getBuiltinModule;\n if (typeof getBuiltinModule === 'function') {\n try {\n return getBuiltinModule.call(process, `node:${id}`) as T;\n } catch {\n return undefined;\n }\n }\n\n // Node < 20.16 / < 22.3 predates `getBuiltinModule`. Real `require` still\n // resolves in the CJS build; in the ESM build this is the shim that throws,\n // which the caller now reports rather than swallows.\n try {\n return require(id) as T;\n } catch {\n return undefined;\n }\n}\n\nexport class ObjectLogger implements Logger {\n private config: Required<Omit<LoggerConfig, 'file' | 'rotation' | 'name'>> & {\n file?: string;\n rotation?: { maxSize: string; maxFiles: number };\n name?: string;\n };\n private bindings: Record<string, any>;\n private fileStream?: any;\n /** Only the logger that opened the stream may close it — children share it. */\n private ownsFileStream = false;\n private fileLoggingDisabled = false;\n\n constructor(config: Partial<LoggerConfig> = {}, bindings: Record<string, any> = {}) {\n this.config = {\n name: config.name,\n level: config.level ?? 'info',\n format: config.format ?? 'pretty',\n redact: config.redact ?? ['password', 'token', 'secret', 'key'],\n sourceLocation: config.sourceLocation ?? false,\n file: config.file,\n rotation: config.rotation ?? { maxSize: '10m', maxFiles: 5 },\n };\n this.bindings = bindings;\n\n if (this.config.file && typeof process !== 'undefined') {\n this.openFileStream(this.config.file);\n }\n }\n\n private openFileStream(path: string) {\n const fs = loadNodeBuiltin<typeof import('node:fs')>('fs');\n const nodePath = loadNodeBuiltin<typeof import('node:path')>('path');\n if (!fs || !nodePath) {\n this.disableFileLogging(path, 'no filesystem access in this runtime');\n return;\n }\n\n try {\n fs.mkdirSync(nodePath.dirname(path), { recursive: true });\n const stream = fs.createWriteStream(path, { flags: 'a' });\n // `createWriteStream` reports open failures (EACCES, EISDIR, …)\n // asynchronously. An 'error' event with no listener is fatal to the\n // process, so file logging must degrade here rather than take the\n // host down.\n stream.on('error', (err: Error) => this.disableFileLogging(path, err.message));\n this.fileStream = stream;\n this.ownsFileStream = true;\n } catch (err) {\n this.disableFileLogging(path, (err as Error).message);\n }\n }\n\n /**\n * Report — once — that an explicitly configured `file` destination is not\n * being written, and stop trying.\n *\n * Deliberately not routed through `write()`: this says the logger cannot\n * honour its own config, so `level` must not filter it. The bare `catch {}`\n * this replaces is exactly how #3110 stayed hidden.\n */\n private disableFileLogging(path: string, reason: string) {\n this.fileStream = undefined;\n this.ownsFileStream = false;\n if (this.fileLoggingDisabled) return;\n this.fileLoggingDisabled = true;\n\n const label = this.config.name ? `[${this.config.name}] ` : '';\n const notice = `${label}logger: file logging disabled — cannot write to ${path}: ${reason}`;\n if (typeof process !== 'undefined' && (process as any).stderr) {\n (process as any).stderr.write(notice + '\\n');\n } else if (typeof console !== 'undefined') {\n console.warn(notice);\n }\n }\n\n private isEnabled(level: LogLevel): boolean {\n return LEVEL_ORDER[level] >= LEVEL_ORDER[this.config.level];\n }\n\n private redactSensitive(obj: any): any {\n if (!obj || typeof obj !== 'object') return obj;\n const redacted = Array.isArray(obj) ? [...obj] : { ...obj };\n for (const key in redacted) {\n const lower = key.toLowerCase();\n if (this.config.redact.some((p: string) => lower.includes(p.toLowerCase()))) {\n redacted[key] = '***REDACTED***';\n } else if (typeof redacted[key] === 'object' && redacted[key] !== null) {\n redacted[key] = this.redactSensitive(redacted[key]);\n }\n }\n return redacted;\n }\n\n private write(level: LogLevel, message: string, meta?: Record<string, any>, error?: Error) {\n if (!this.isEnabled(level)) return;\n\n const context = this.redactSensitive({\n ...this.bindings,\n ...meta,\n ...(error ? { error: { message: error.message, stack: error.stack } } : {}),\n });\n\n const hasContext = Object.keys(context).length > 0;\n const ts = new Date().toISOString();\n\n const isErrorLevel = level === 'error' || level === 'fatal';\n const proc = typeof process !== 'undefined' ? (process as any) : undefined;\n const stream = proc ? (isErrorLevel ? proc.stderr : proc.stdout) : undefined;\n\n let line: string; // console output — may carry ANSI color\n let plainLine: string; // file output — never colored\n\n if (this.config.format === 'json') {\n line = plainLine = JSON.stringify({\n time: ts,\n level,\n ...(this.config.name ? { name: this.config.name } : {}),\n msg: message,\n ...context,\n });\n } else if (this.config.format === 'text') {\n const parts = [ts, level.toUpperCase(), message];\n if (hasContext) parts.push(JSON.stringify(context));\n line = plainLine = parts.join(' | ');\n } else {\n // pretty\n const label = this.config.name ? `[${this.config.name}] ` : '';\n const head = `${ts} ${level.toUpperCase()}`;\n let tail = ` ${label}${message}`;\n if (hasContext) tail += ` ${JSON.stringify(context)}`;\n plainLine = head + tail;\n const color = LEVEL_COLORS[level] || '';\n line = color && colorEnabled(stream) ? `${color}${head}${RESET}${tail}` : plainLine;\n }\n\n // Browser-safe output: prefer process streams when available, otherwise\n // fall back to console. `process` may be missing entirely (browsers) or\n // present without stdio streams (bundler shims) — both fall through to\n // console. The previous unguarded `process.stderr?.write` threw\n // `ReferenceError: process is not defined` in browsers because\n // `process` itself is the missing global, not just its `stderr` field.\n if (stream) {\n stream.write(line + '\\n');\n } else if (typeof console !== 'undefined') {\n const fn =\n level === 'error' || level === 'fatal' ? console.error\n : level === 'warn' ? console.warn\n : level === 'debug' ? console.debug\n : console.log;\n fn(line);\n }\n\n if (this.fileStream) {\n this.fileStream.write(plainLine + '\\n');\n }\n }\n\n debug(message: string, meta?: Record<string, any>): void {\n this.write('debug', message, meta);\n }\n\n info(message: string, meta?: Record<string, any>): void {\n this.write('info', message, meta);\n }\n\n warn(message: string, meta?: Record<string, any>): void {\n this.write('warn', message, meta);\n }\n\n error(message: string, errorOrMeta?: Error | Record<string, any>, meta?: Record<string, any>): void {\n if (errorOrMeta instanceof Error) {\n this.write('error', message, meta, errorOrMeta);\n } else {\n this.write('error', message, errorOrMeta);\n }\n }\n\n fatal(message: string, errorOrMeta?: Error | Record<string, any>, meta?: Record<string, any>): void {\n if (errorOrMeta instanceof Error) {\n this.write('fatal', message, meta, errorOrMeta);\n } else {\n this.write('fatal', message, errorOrMeta);\n }\n }\n\n log(message: string, ...args: any[]): void {\n this.info(message, args.length > 0 ? { args } : undefined);\n }\n\n child(context: Record<string, any>): ObjectLogger {\n // Construct without `file`, then share the parent's stream: the\n // constructor opens eagerly, so passing `file` through would open a\n // second stream per child and immediately orphan it. That leak was\n // unreachable while #3110 kept the ESM open path dead.\n const child = new ObjectLogger({ ...this.config, file: undefined }, { ...this.bindings, ...context });\n child.config.file = this.config.file;\n child.fileStream = this.fileStream;\n return child;\n }\n\n withTrace(traceId: string, spanId?: string): ObjectLogger {\n return this.child({ traceId, spanId });\n }\n\n async destroy(): Promise<void> {\n const stream = this.fileStream;\n this.fileStream = undefined;\n // Children share the opener's stream; if they closed it too, one child's\n // teardown would end file logging for the parent and every sibling,\n // whose writes then land on a closed stream and only trip the 'error'\n // handler above.\n if (!stream || !this.ownsFileStream) return;\n this.ownsFileStream = false;\n await new Promise<void>((resolve) => stream.end(resolve));\n }\n}\n\nexport function createLogger(config?: Partial<LoggerConfig>): ObjectLogger {\n return new ObjectLogger(config);\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/core",
3
- "version": "15.1.1",
3
+ "version": "16.0.0-rc.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Microkernel Core for ObjectStack",
6
6
  "type": "module",
@@ -20,12 +20,13 @@
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/node": "^26.1.1",
23
+ "esbuild": "^0.28.1",
23
24
  "typescript": "^6.0.3",
24
25
  "vitest": "^4.1.10"
25
26
  },
26
27
  "dependencies": {
27
28
  "zod": "^4.4.3",
28
- "@objectstack/spec": "15.1.1"
29
+ "@objectstack/spec": "16.0.0-rc.0"
29
30
  },
30
31
  "keywords": [
31
32
  "objectstack",