Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -260,4 +260,22 @@ export default [
'@typescript-eslint/no-empty-object-type': 'off',
},
},
// Logging discipline: shipped package source must use the shared logger
// (`@superdoc/common/logger`) instead of raw `console.*`, so embedding
// SuperDoc does not spam the host console and output stays level-gated and
// redirectable. Build scripts (scripts/**) and tests are exempt.
{
files: ['packages/*/src/**/*.{js,ts}', 'shared/**/*.{js,ts}'],
ignores: [
// The logger implementation itself owns the only sanctioned console sink.
'shared/common/logger.ts',
// @superdoc-dev/ai is deprecated and keeps its own legacy logger.
'packages/ai/src/shared/logger.ts',
// Zero-dependency security leaf: a single env-gated diagnostic warning.
'shared/url-validation/index.js',
],
rules: {
'no-console': 'error',
},
},
];
1 change: 1 addition & 0 deletions packages/collaboration-yjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"access": "public"
},
"dependencies": {
"@superdoc/common": "workspace:*",
"lib0": "catalog:",
"y-protocols": "catalog:",
"yjs": "catalog:"
Expand Down
23 changes: 11 additions & 12 deletions packages/collaboration-yjs/src/internal-logger/logger.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
const COLORS = {
ConnectionHandler: '\x1b[34m', // blue
DocumentManager: '\x1b[32m', // green
SuperDocCollaboration: '\x1b[35m', // magenta
reset: '\x1b[0m',
};
import { createLogger as createBaseLogger } from '@superdoc/common/logger';

export type Logger = (...args: unknown[]) => void;

export function createLogger(label: keyof typeof COLORS | string): Logger {
const color = (COLORS as Record<string, string>)[label] || COLORS.reset;

return (...args: unknown[]) => {
console.log(`${color}[${label}]${COLORS.reset}`, ...args);
};
/**
* Always-on, label-prefixed logger for the collaboration server.
*
* Thin adapter over the shared `@superdoc/common/logger`. The level is pinned
* to `info` so server diagnostics stay visible regardless of the global level,
* preserving the previous always-print behavior.
*/
export function createLogger(label: string): Logger {
const base = createBaseLogger(label, { level: 'info' });
return (...args: unknown[]) => base.info(...args);
}
7 changes: 5 additions & 2 deletions packages/collaboration-yjs/src/shared-doc/callback.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import http from 'node:http';
import * as number from 'lib0/number';
import { createLogger } from '@superdoc/common/logger';
import type { SharedSuperDoc } from './shared-doc.js';

const log = createLogger('callback');

type CallbackObjects = Record<string, string>;
type CallbackPayload = {
room: string;
Expand Down Expand Up @@ -54,12 +57,12 @@ const callbackRequest = (url: URL, timeout: number, data: CallbackPayload) => {
};
const req = http.request(options);
req.on('timeout', () => {
console.warn('Callback request timed out.');
log.warn('Callback request timed out.');
req.abort();
});
req.on('error', (e) => {
const sanitizedError = String(e).replace(/\n|\r/g, '');
console.error('Callback request error:', sanitizedError);
log.error('Callback request error:', sanitizedError);
req.abort();
});
req.write(serialized);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -362,11 +362,11 @@ describe('callback handler', () => {
});

req.trigger('timeout');
expect(consoleWarn).toHaveBeenCalledWith('Callback request timed out.');
expect(consoleWarn).toHaveBeenCalledWith('[callback]', 'Callback request timed out.');
expect(req.abort).toHaveBeenCalledTimes(1);

req.trigger('error', new Error('failed\nreason'));
expect(consoleError).toHaveBeenCalledWith('Callback request error:', 'Error: failedreason');
expect(consoleError).toHaveBeenCalledWith('[callback]', 'Callback request error:', 'Error: failedreason');
expect(req.abort).toHaveBeenCalledTimes(2);

consoleWarn.mockRestore();
Expand Down Expand Up @@ -545,7 +545,7 @@ describe('SharedSuperDoc', () => {
}
messageHandler(new Uint8Array([99]));

expect(consoleWarn).toHaveBeenCalledWith('Unknown message type:', 99);
expect(consoleWarn).toHaveBeenCalledWith('[shared-doc]', 'Unknown message type:', 99);
consoleWarn.mockRestore();
});

Expand All @@ -569,7 +569,7 @@ describe('SharedSuperDoc', () => {
readSyncBehavior = 'throw';
messageHandler(new Uint8Array([0]));

expect(consoleError).toHaveBeenCalledWith('Error in messageListener:', expect.any(Error));
expect(consoleError).toHaveBeenCalledWith('[shared-doc]', 'Error in messageListener:', expect.any(Error));
expect(errorListener).toHaveBeenCalledWith(expect.any(Error));
consoleError.mockRestore();
});
Expand Down
7 changes: 5 additions & 2 deletions packages/collaboration-yjs/src/shared-doc/shared-doc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ import { createEncoder, writeVarUint, writeVarUint8Array, toUint8Array, length a
import { readVarUint8Array, createDecoder, readVarUint } from 'lib0/decoding';
import { Awareness, encodeAwarenessUpdate, removeAwarenessStates, applyAwarenessUpdate } from 'y-protocols/awareness';
import { Doc as YDoc } from 'yjs';
import { createLogger } from '@superdoc/common/logger';
import { callbackHandler, isCallbackSet } from './callback.js';
import { debouncer } from './utils.js';
import { messageSync, messageAwareness, wsReadyStateConnecting, wsReadyStateOpen } from './constants.js';
import type { CollaborationWebSocket } from '../types/service-types.js';

const log = createLogger('shared-doc');

type AwarenessChange = { added: number[]; updated: number[]; removed: number[] };

interface YDocWithEmit extends YDoc {
Expand Down Expand Up @@ -153,10 +156,10 @@ const messageListener = (conn: CollaborationWebSocket, doc: SharedSuperDoc, mess
}

default:
console.warn('Unknown message type:', messageType);
log.warn('Unknown message type:', messageType);
}
} catch (err) {
console.error('Error in messageListener:', err);
log.error('Error in messageListener:', err);
(doc as YDocWithEmit).emit('error', [err]);
}
};
12 changes: 7 additions & 5 deletions packages/collaboration-yjs/src/tests/internal-logger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,29 @@ describe('createLogger', () => {
let consoleSpy: ReturnType<typeof vi.spyOn>;

beforeEach(() => {
consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
// The logger now routes through the shared @superdoc/common logger, whose
// console sink writes `info`-level records via console.info.
consoleSpy = vi.spyOn(console, 'info').mockImplementation(() => {});
});

afterEach(() => {
consoleSpy.mockRestore();
});

test('logs with configured color for known label', () => {
test('prefixes output with the label', () => {
const logger = createLogger('ConnectionHandler');

logger('connected', 123);

expect(consoleSpy).toHaveBeenCalledWith('\x1b[34m[ConnectionHandler]\x1b[0m', 'connected', 123);
expect(consoleSpy).toHaveBeenCalledWith('[ConnectionHandler]', 'connected', 123);
});

test('falls back to reset color for unknown label', () => {
test('prefixes output for any label', () => {
const logger = createLogger('Custom');

logger('info');

expect(consoleSpy).toHaveBeenCalledWith('\x1b[0m[Custom]\x1b[0m', 'info');
expect(consoleSpy).toHaveBeenCalledWith('[Custom]', 'info');
});

test('returns a stable logging function', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
* @module debounced-passes
*/

import { createLogger } from '@superdoc/common/logger';

const log = createLogger('layout-bridge:debounced-passes');

/**
* Configuration for a debounced pass.
*/
Expand Down Expand Up @@ -157,9 +161,7 @@ export class DebouncedPassManager {
} catch (error) {
// Silently handle errors to prevent cascading failures
// In production, this would be logged
if (typeof console !== 'undefined' && console.error) {
console.error(`Error executing debounced pass "${passId}":`, error);
}
log.error(`Error executing debounced pass "${passId}":`, error);
}
}

Expand Down
10 changes: 7 additions & 3 deletions packages/layout-engine/layout-bridge/src/dirty-tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
* @module dirty-tracker
*/

import { createLogger } from '@superdoc/common/logger';

const log = createLogger('layout-bridge:dirty-tracker');

/**
* Represents a range of dirty content with reason for tracking.
*/
Expand Down Expand Up @@ -50,7 +54,7 @@ export class DirtyTracker {
*/
markPageDirty(pageIndex: number, reason: DirtyRange['reason']): void {
if (pageIndex < 0) {
console.warn(`[DirtyTracker] Invalid page index: ${pageIndex}. Must be non-negative. Ignoring.`);
log.warn(`[DirtyTracker] Invalid page index: ${pageIndex}. Must be non-negative. Ignoring.`);
return; // Ignore invalid page indices
}

Expand All @@ -76,7 +80,7 @@ export class DirtyTracker {
*/
markBlocksDirty(startBlock: number, endBlock: number, reason: DirtyRange['reason']): void {
if (startBlock < 0 || endBlock < startBlock) {
console.warn(
log.warn(
`[DirtyTracker] Invalid block range: [${startBlock}, ${endBlock}]. Start must be non-negative and end must be >= start. Ignoring.`,
);
return; // Ignore invalid ranges
Expand Down Expand Up @@ -105,7 +109,7 @@ export class DirtyTracker {
*/
markDirtyFrom(startPage: number, reason: DirtyRange['reason']): void {
if (startPage < 0) {
console.warn(`[DirtyTracker] Invalid start page: ${startPage}. Must be non-negative. Ignoring.`);
log.warn(`[DirtyTracker] Invalid start page: ${startPage}. Must be non-negative. Ignoring.`);
return; // Ignore invalid page indices
}

Expand Down
5 changes: 4 additions & 1 deletion packages/layout-engine/layout-bridge/src/dom-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,15 @@
*/

import { DOM_CLASS_NAMES, STRUCTURED_CONTENT_CHROME_LABEL_CLASS_NAMES } from '@superdoc/dom-contract';
import { createLogger } from '@superdoc/common/logger';

const logger = createLogger('layout-bridge:dom-map');

// Debug logging for click-to-position pipeline (disabled - enable for debugging)
const DEBUG_CLICK_MAPPING = false;
const log = (...args: unknown[]) => {
if (DEBUG_CLICK_MAPPING) {
console.log('[DOM-MAP]', ...args);
logger.debug(...args);
}
};

Expand Down
20 changes: 12 additions & 8 deletions packages/layout-engine/layout-bridge/src/focus-watchdog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
* @module focus-watchdog
*/

import { createLogger } from '@superdoc/common/logger';

const log = createLogger('layout-bridge:focus-watchdog');

/**
* Configuration for focus watchdog.
*/
Expand Down Expand Up @@ -115,7 +119,7 @@ export class FocusWatchdog {
}

if (!this.expectedFocusElement) {
console.warn('FocusWatchdog: Cannot start without expected focus element');
log.warn('FocusWatchdog: Cannot start without expected focus element');
return;
}

Expand All @@ -124,7 +128,7 @@ export class FocusWatchdog {
this.check();
}, this.config.checkInterval);

console.log('FocusWatchdog: Started monitoring');
log.debug('FocusWatchdog: Started monitoring');
}

/**
Expand All @@ -148,7 +152,7 @@ export class FocusWatchdog {
}

this.running = false;
console.log('FocusWatchdog: Stopped monitoring');
log.debug('FocusWatchdog: Stopped monitoring');
}

/**
Expand Down Expand Up @@ -234,15 +238,15 @@ export class FocusWatchdog {
const restored = document.activeElement === this.expectedFocusElement;

if (restored) {
console.log('FocusWatchdog: Focus restored successfully');
log.debug('FocusWatchdog: Focus restored successfully');
this.config.onRecovery();
return true;
} else {
console.warn('FocusWatchdog: Failed to restore focus');
log.warn('FocusWatchdog: Failed to restore focus');
return false;
}
} catch (err) {
console.error('FocusWatchdog: Error restoring focus:', err);
log.error('FocusWatchdog: Error restoring focus:', err);
return false;
}
}
Expand All @@ -255,7 +259,7 @@ export class FocusWatchdog {
private handleDrift(target: Element | null): void {
this.driftCount++;

console.warn(
log.warn(
`FocusWatchdog: Focus drift detected (${this.driftCount}/${this.config.maxDriftCount})`,
'Target:',
target,
Expand All @@ -266,7 +270,7 @@ export class FocusWatchdog {

// Attempt recovery if threshold reached
if (this.driftCount >= this.config.maxDriftCount) {
console.warn('FocusWatchdog: Drift threshold reached, attempting recovery');
log.warn('FocusWatchdog: Drift threshold reached, attempting recovery');
this.restoreFocus();
this.driftCount = 0; // Reset after recovery attempt
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
* @module font-metrics-cache
*/

import { createLogger } from '@superdoc/common/logger';

const log = createLogger('layout-bridge:font-metrics-cache');

/**
* Metrics for a specific font configuration.
*/
Expand Down Expand Up @@ -164,7 +168,7 @@ export class FontMetricsCache {
for (const config of fonts) {
// Validate fontKey format (should be "family|size|weight|style")
if (!this.isValidFontKey(config.fontKey)) {
console.warn(
log.warn(
`[FontMetricsCache] Invalid fontKey format: "${config.fontKey}". Expected format: "family|size|weight|style". Skipping.`,
);
continue;
Expand Down
Loading
Loading