@monochromatic-dev/module-logger 0.2.0 → 0.3.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/CHANGELOG.md +11 -0
- package/README.md +33 -8
- package/dist/final/neutral/browser.d.mts +60 -0
- package/dist/final/neutral/browser.mjs +1 -0
- package/dist/final/neutral/index.d.mts +360 -594
- package/dist/final/neutral/index.mjs +2 -3
- package/dist/final/neutral/indexed-db-hsIfv7Cv.mjs +2 -0
- package/dist/final/neutral/types-BkkBXgY3.d.mts +76 -0
- package/dist/final/node/file-CRGb1hDK.mjs +1 -0
- package/dist/final/node/index.d.mts +360 -594
- package/dist/final/node/index.mjs +3 -3
- package/dist/final/node/node.d.mts +103 -0
- package/dist/final/node/node.mjs +1 -0
- package/dist/final/node/types-BkkBXgY3.d.mts +76 -0
- package/package.json +18 -4
- package/src/artifact-platform-split.unit.test.ts +140 -0
- package/src/browser.ts +14 -0
- package/src/create-logger.ts +183 -183
- package/src/create-logger.unit.test.ts +112 -112
- package/src/default-sinks.neutral.ts +34 -0
- package/src/default-sinks.node.ts +32 -0
- package/src/error-format.ts +23 -23
- package/src/logger.ts +23 -50
- package/src/node.ts +23 -0
- package/src/sink/console-control-chars.ts +64 -64
- package/src/sink/console-control-chars.unit.test.ts +14 -14
- package/src/sink/console.ts +194 -194
- package/src/sink/console.unit.test.ts +18 -18
- package/src/sink/file.ts +136 -140
- package/src/sink/file.unit.test.ts +19 -26
- package/src/sink/index.ts +4 -7
- package/src/sink/indexed-db-util.ts +42 -42
- package/src/sink/indexed-db.browser.test.ts +7 -7
- package/src/sink/indexed-db.ts +109 -109
- package/src/sink/indexed-db.unit.test.ts +5 -13
- package/src/sink/local-storage-key.ts +73 -73
- package/src/sink/local-storage-key.unit.test.ts +8 -8
- package/src/sink/local-storage-quota.ts +37 -37
- package/src/sink/local-storage-quota.unit.test.ts +8 -8
- package/src/sink/local-storage-store.ts +113 -113
- package/src/sink/local-storage-store.unit.test.ts +35 -35
- package/src/sink/local-storage.ts +72 -72
- package/src/sink/local-storage.unit.test.ts +27 -27
- package/src/sink/noop.ts +20 -20
- package/src/sink/noop.unit.test.ts +1 -1
- package/src/sink/opfs.browser.test.ts +7 -7
- package/src/sink/opfs.ts +62 -62
- package/src/sink/opfs.unit.test.ts +5 -13
- package/src/sink/record-buffer.ts +84 -84
- package/src/sink/record-buffer.unit.test.ts +20 -20
- package/src/sink/session-storage-quota.ts +34 -34
- package/src/sink/session-storage-quota.unit.test.ts +8 -8
- package/src/sink/session-storage-store.ts +72 -72
- package/src/sink/session-storage.ts +48 -48
- package/src/sink/session-storage.unit.test.ts +39 -39
- package/src/sink/web-storage-quota-error.ts +22 -22
- package/src/sink/web-storage-quota-error.unit.test.ts +2 -2
- package/src/sink/web-storage-runtime.ts +24 -24
- package/src/startup.unit.test.ts +18 -18
- package/src/tagged.ts +35 -35
- package/src/tagged.unit.test.ts +8 -8
- package/src/types.ts +39 -39
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{t as __exportAll}from"./rolldown-runtime-5duEfhBv.mjs";async function withTimeout({promise,ms,label}){let{promise:timeoutPromise,reject}=Promise.withResolvers(),timer=setTimeout(function(){reject(Error(`Timed out after ${String(ms)}ms: ${label}`))},ms);using _cleanup={[Symbol.dispose](){clearTimeout(timer)}};return await Promise.race([promise,timeoutPromise])}
|
|
2
|
-
`);if(!(level===`debug`&&writeDebugRunToProcessStderr(text)))try{let method=LEVEL_TO_CONSOLE_METHOD[level],consoleFn=console[method];typeof consoleFn==`function`&&consoleFn.call(console,text)}catch(error){reportLoggerInternalError({context:`console method call failed while emitting log run`,error})}}function groupRuns(records){return records.reduce(function(runs,record){let tail=runs.at(-1);return tail!==void 0&&tail.level===record.level?(tail.records.push(record),runs):(runs.push({level:record.level,records:[record]}),runs)},[])}function verifyConsole(){try{return typeof console>`u`||typeof(hasProcessStderr()?console.info:console.debug)!=`function`||typeof queueMicrotask!=`function`?Promise.resolve(!1):Promise.resolve(!0)}catch(error){return reportLoggerInternalError({context:`console sink verification failed`,error}),Promise.resolve(!1)}}function createConsoleSink(){let state={buffer:[],scheduled:!1,verboseCache:VERBOSE_UNCOMPUTED};function getVerbose(){let cached=state.verboseCache;if(cached!==VERBOSE_UNCOMPUTED)return cached;let computed=detectVerbose();return state.verboseCache=computed,computed}function flushBuffer(){if(state.scheduled=!1,state.buffer.length===0)return;let records=state.buffer.splice(0);for(let run of groupRuns(records))emitRun({level:run.level,records:run.records})}function write(record){return!getVerbose()&&SILENT_LEVELS.has(record.level)||record.level===`warn`&&isWarnSuppressed()?Promise.resolve():(state.buffer.push(record),state.scheduled||(state.scheduled=!0,queueMicrotask(flushBuffer)),Promise.resolve())}function flush(){return flushBuffer(),Promise.resolve()}return{flush,verify:verifyConsole,write}}
|
|
3
|
-
`);entries.length=0,bufferState.chars=0,onFlush(batch)}function scheduleDeadlineFlush(){if(bufferState.timer!==void 0)return;let timer=globalThis.setTimeout(drain,250);isUnrefableTimer(timer)&&timer.unref(),bufferState.timer=timer}function add(entry){entries.length>0&&charsWith(entry.serialized)>FLUSH_BUFFER_CAP_CHARS&&drain(),bufferState.chars=charsWith(entry.serialized),entries.push(entry.serialized),FLUSH_IMMEDIATELY_BY_LEVEL[entry.level]||bufferState.chars>=FLUSH_BUFFER_CAP_CHARS?drain():scheduleDeadlineFlush()}return globalThis.addEventListener?.(`pagehide`,drain),globalThis.document?.addEventListener(`visibilitychange`,function(){globalThis.document?.visibilityState===`hidden`&&drain()}),{add,drain}}const BATCH_STORE=`batch`,MAX_STORED_BATCHES=2048;async function openLogDatabase(){let request=globalThis.indexedDB.open(`monochromatic.log`,1);return request.onupgradeneeded=function(){request.result.createObjectStore(BATCH_STORE,{autoIncrement:!0})},await awaitRequest(request)}async function persistBatch({database,batch}){let transaction=database.transaction(BATCH_STORE,`readwrite`),store=transaction.objectStore(BATCH_STORE);store.add(batch);let count=await awaitRequest(store.count());if(count>MAX_STORED_BATCHES){let newestStale=(await awaitRequest(store.getAllKeys(null,count-MAX_STORED_BATCHES))).at(-1);newestStale!==void 0&&store.delete(IDBKeyRange.upperBound(newestStale))}await awaitTransaction(transaction)}function createIndexedDbSink(){let state={},pendingBatchWrites=new Set;async function verify(){try{if(globalThis.indexedDB===void 0)return!1;let database=await openLogDatabase(),transaction=database.transaction(BATCH_STORE,`readwrite`),store=transaction.objectStore(BATCH_STORE),probeValue=`probe-${Date.now()}`,probeKey=await awaitRequest(store.add(probeValue)),readBack=await awaitRequest(store.get(probeKey));return store.delete(probeKey),await awaitTransaction(transaction),readBack===probeValue&&(state.database=database,!0)}catch(error){return`indexedDB`in globalThis&&reportLoggerInternalError({context:`IndexedDB sink verification failed`,error}),!1}}async function writeBatch(batch){if(state.database)try{await persistBatch({database:state.database,batch})}catch(error){reportLoggerInternalError({context:`IndexedDB sink record write failed`,error})}}async function removePendingWhenSettled(pending){await pending,pendingBatchWrites.delete(pending)}function handOffBatch(batch){let pending=writeBatch(batch);pendingBatchWrites.add(pending),removePendingWhenSettled(pending)}let buffer=createRecordBuffer({onFlush:handOffBatch});function write(record){return buffer.add({level:record.level,serialized:JSON.stringify(record)}),Promise.resolve()}async function flush(){buffer.drain();let writes=[...pendingBatchWrites];await Promise.all(writes)}return{flush,verify,write}}function isDigits(text){if(text.length===0)return!1;for(let character of text)if(character<`0`||character>`9`)return!1;return!0}function buildLogKey({stamp,nonce,index}){return`monochromatic.log.${stamp}.${nonce}.${index}`}function parseLogKey(key){if(!key.startsWith(`monochromatic.log.`))return{};let segments=key.slice(18).split(`.`);if(segments.length!==3)return{};let[stampText,nonce,indexText]=segments;return stampText===void 0||nonce===void 0||indexText===void 0||!isDigits(stampText)||nonce.length===0||!isDigits(indexText)?{}:{parsed:{key,stamp:Math.trunc(Number(stampText)),nonce,index:Math.trunc(Number(indexText))}}}function compareLogKeys({first,second}){return first.stamp===second.stamp?first.nonce===second.nonce?first.index-second.index:first.nonce<second.nonce?-1:1:first.stamp-second.stamp}function detectWebStorageRuntime(){return`Deno`in globalThis?`deno`:`Bun`in globalThis?`bun`:typeof process<`u`&&typeof process.versions.node==`string`?`node`:`document`in globalThis?`browser`:`unknown`}const RUNTIME_QUOTA_CHARS$1={browser:5242880,bun:1/0,deno:10477569,node:5242880,unknown:1/0};function detectLocalStorageQuotaChars(){return RUNTIME_QUOTA_CHARS$1[detectWebStorageRuntime()]}const QUOTA_EXCEEDED_NAMES=new Set([`QuotaExceededError`,`NS_ERROR_DOM_QUOTA_REACHED`]);function isQuotaExceededError(error){return typeof error==`object`&&!!error&&`name`in error&&typeof error.name==`string`&"A_EXCEEDED_NAMES.has(error.name)}function createLocalStorageStore(){let runIdentity={stamp:Date.now(),nonce:Math.random().toString(36).slice(2,6).padEnd(4,`0`)},state={lineCounter:0,oldestIndex:0,usedChars:0,reportedFailure:!1,adoptedPrior:!1},prior={entries:[],cursor:0},capChars=detectLocalStorageQuotaChars()/2;function ownKey(index){return buildLogKey({stamp:runIdentity.stamp,nonce:runIdentity.nonce,index})}function adoptPriorEntries(){let total=globalThis.localStorage.length,found=[];for(let slot=0;slot<total;slot++){let key=globalThis.localStorage.key(slot);if(key===null)continue;let{parsed}=parseLogKey(key);if(parsed===void 0||parsed.stamp===runIdentity.stamp&&parsed.nonce===runIdentity.nonce)continue;let value=globalThis.localStorage.getItem(key);value!==null&&found.push({...parsed,chars:value.length})}prior.entries=found.toSorted(function(first,second){return compareLogKeys({first,second})}),state.usedChars+=found.reduce(function(sum,entry){return sum+entry.chars},0)}function hasEvictable(){let priorCount=prior.entries.length;return prior.cursor<priorCount||state.oldestIndex<state.lineCounter}function evictOldestPrior(){let entry=prior.entries[prior.cursor];return entry!==void 0&&(prior.cursor++,globalThis.localStorage.removeItem(entry.key),state.usedChars=Math.max(0,state.usedChars-entry.chars),!0)}function evictOldestOwn(){let key=ownKey(state.oldestIndex),evicted=globalThis.localStorage.getItem(key);globalThis.localStorage.removeItem(key),state.oldestIndex++,evicted!==null&&(state.usedChars=Math.max(0,state.usedChars-evicted.length))}function evictOldest(){evictOldestPrior()||state.oldestIndex<state.lineCounter&&evictOldestOwn()}function persist(batch){state.adoptedPrior||(state.adoptedPrior=!0,adoptPriorEntries());let batchChars=batch.length;for(;hasEvictable()&&state.usedChars+batchChars>capChars;)evictOldest();let maxWriteAttempts=prior.entries.length-prior.cursor+(state.lineCounter-state.oldestIndex)+1;for(let writeAttempt=0;writeAttempt<maxWriteAttempts;writeAttempt++)try{globalThis.localStorage.setItem(ownKey(state.lineCounter),batch),state.lineCounter++,state.usedChars+=batchChars,state.reportedFailure=!1;return}catch(error){if(isQuotaExceededError(error)&&hasEvictable()){evictOldest();continue}state.reportedFailure||=(reportLoggerInternalError({context:`localStorage sink record write failed (repeats suppressed until a write next succeeds)`,error}),!0);return}}return{persist}}const NODE_LOCALSTORAGE_FLAG=`--localstorage-file`;function nodeWithoutLocalStorageFile(){if(detectWebStorageRuntime()!==`node`||`document`in globalThis)return!1;let flaggedInExecArgv=process.execArgv.some(function(argument){return argument.startsWith(NODE_LOCALSTORAGE_FLAG)}),flaggedInNodeOptions=(process.env.NODE_OPTIONS??``).includes(NODE_LOCALSTORAGE_FLAG);return!(flaggedInExecArgv||flaggedInNodeOptions)}function verifyLocalStorage(){if(nodeWithoutLocalStorageFile())return Promise.resolve(!1);try{let testKey=`__monochromatic_verify__`,testValue=`test-${Date.now()}`;globalThis.localStorage.setItem(testKey,testValue);let readBack=globalThis.localStorage.getItem(testKey);return globalThis.localStorage.removeItem(testKey),Promise.resolve(readBack===testValue)}catch(error){return`localStorage`in globalThis&&reportLoggerInternalError({context:`localStorage sink verification failed`,error}),Promise.resolve(!1)}}function createLocalStorageSink(){let buffer=createRecordBuffer({onFlush:createLocalStorageStore().persist});function write(record){return buffer.add({level:record.level,serialized:JSON.stringify(record)}),Promise.resolve()}function flush(){return buffer.drain(),Promise.resolve()}return{flush,verify:verifyLocalStorage,write}}const RUNTIME_QUOTA_CHARS={browser:5242880,bun:1/0,deno:10485760,node:5242880,unknown:1/0};function detectSessionStorageQuotaChars(){return RUNTIME_QUOTA_CHARS[detectWebStorageRuntime()]}function storageKey(index){return`monochromatic.log.${index}`}function createSessionStorageStore(){let state={lineCounter:0,oldestIndex:0,usedChars:0,reportedFailure:!1},capChars=detectSessionStorageQuotaChars()/2;function evictOldest(){let key=storageKey(state.oldestIndex),evicted=globalThis.sessionStorage.getItem(key);globalThis.sessionStorage.removeItem(key),state.oldestIndex++,evicted!==null&&(state.usedChars=Math.max(0,state.usedChars-evicted.length))}function persist(batch){let batchChars=batch.length;for(;state.oldestIndex<state.lineCounter&&state.usedChars+batchChars>capChars;)evictOldest();let maxWriteAttempts=state.lineCounter-state.oldestIndex+1;for(let writeAttempt=0;writeAttempt<maxWriteAttempts;writeAttempt++)try{globalThis.sessionStorage.setItem(storageKey(state.lineCounter),batch),state.lineCounter++,state.usedChars+=batchChars,state.reportedFailure=!1;return}catch(error){if(isQuotaExceededError(error)&&state.oldestIndex<state.lineCounter){evictOldest();continue}state.reportedFailure||=(reportLoggerInternalError({context:`sessionStorage sink record write failed (repeats suppressed until a write next succeeds)`,error}),!0);return}}return{persist}}function verifySessionStorage(){try{let testKey=`__monochromatic_verify__`,testValue=`test-${Date.now()}`;globalThis.sessionStorage.setItem(testKey,testValue);let readBack=globalThis.sessionStorage.getItem(testKey);return globalThis.sessionStorage.removeItem(testKey),Promise.resolve(readBack===testValue)}catch(error){return`sessionStorage`in globalThis&&reportLoggerInternalError({context:`sessionStorage sink verification failed`,error}),Promise.resolve(!1)}}function createSessionStorageSink(){let buffer=createRecordBuffer({onFlush:createSessionStorageStore().persist});function write(record){return buffer.add({level:record.level,serialized:JSON.stringify(record)}),Promise.resolve()}function flush(){return buffer.drain(),Promise.resolve()}return{flush,verify:verifySessionStorage,write}}const{initPromise:defaultInitPromise,logger:defaultLogger}=createLogger({sinks:[createConsoleSink(),createIndexedDbSink(),createSessionStorageSink(),createLocalStorageSink(),createFileSink()]}),initPromise=defaultInitPromise,logger=defaultLogger;function verify(){return Promise.resolve(!0)}function write(_record){return Promise.resolve()}function createNoopSink(){return{verify,write}}function createOpfsSink(){let state={},pendingBatchWrites=new Set;async function verify(){try{let opfsRoot=await navigator.storage.getDirectory(),timestamp=new Date().toISOString().replaceAll(`:`,`-`),fileHandle=await opfsRoot.getFileHandle(`monochromatic-${timestamp}.log.jsonl`,{create:!0}),probeWritable=await fileHandle.createWritable({keepExistingData:!0}),testData=`{"test":true,"timestamp":${Date.now()}}\n`;await probeWritable.write(testData),await probeWritable.close();let available=(await(await fileHandle.getFile()).text()).includes(`"test":true`);return available&&(state.writable=await fileHandle.createWritable({keepExistingData:!0})),available}catch(error){let opfsStorage=globalThis.navigator?.storage;return opfsStorage!==void 0&&`getDirectory`in opfsStorage&&reportLoggerInternalError({context:`OPFS sink verification failed`,error}),!1}}async function writeBatch(batch){if(state.writable)try{await state.writable.write(`${batch}\n`)}catch(error){reportLoggerInternalError({context:`OPFS sink record write failed`,error})}}async function removePendingWhenSettled(pending){await pending,pendingBatchWrites.delete(pending)}function handOffBatch(batch){let pending=writeBatch(batch);pendingBatchWrites.add(pending),removePendingWhenSettled(pending)}let buffer=createRecordBuffer({onFlush:handOffBatch});function write(record){return buffer.add({level:record.level,serialized:JSON.stringify(record)}),Promise.resolve()}async function flush(){buffer.drain();let writes=[...pendingBatchWrites];await Promise.all(writes)}return{flush,verify,write}}var sink_exports=__exportAll({NO_NODE_MODULES_FOUND:()=>NO_NODE_MODULES_FOUND,createConsoleSink:()=>createConsoleSink,createFileSink:()=>createFileSink,createIndexedDbSink:()=>createIndexedDbSink,createLocalStorageSink:()=>createLocalStorageSink,createNoopSink:()=>createNoopSink,createOpfsSink:()=>createOpfsSink,createSessionStorageSink:()=>createSessionStorageSink,findNodeModulesUp:()=>findNodeModulesUp});function tagged({tag,l=logger}){let prefix=`[${tag}] `;return{debug:function(message){l.debug(`${prefix}${message}`)},error:function(message){l.error(`${prefix}${message}`)},fatal:function(message){l.fatal(`${prefix}${message}`)},flush:function(){return l.flush()},info:function(message){l.info(`${prefix}${message}`)},trace:function(message){l.trace(`${prefix}${message}`)},warn:function(message){l.warn(`${prefix}${message}`)}}}export{DEFAULT_FLUSH_DEADLINE_MS,DEFAULT_VERIFY_TIMEOUT_MS,STARTUP_BUFFER_CAP,buildLogKey as _buildLogKey,compareLogKeys as _compareLogKeys,createLocalStorageStore as _createLocalStorageStore,createRecordBuffer as _createRecordBuffer,detectLocalStorageQuotaChars as _detectLocalStorageQuotaChars,detectSessionStorageQuotaChars as _detectSessionStorageQuotaChars,isQuotaExceededError as _isQuotaExceededError,neutralizeControlCharacters as _neutralizeControlCharacters,parseLogKey as _parseLogKey,createLogger,initPromise,logger,sink_exports as sinks,tagged};
|
|
1
|
+
import{t as __exportAll}from"./rolldown-runtime-5duEfhBv.mjs";import{i as reportLoggerInternalError,n as createFileSink}from"./file-CRGb1hDK.mjs";async function withTimeout({promise,ms,label}){let{promise:timeoutPromise,reject}=Promise.withResolvers(),timer=setTimeout(function(){reject(Error(`Timed out after ${String(ms)}ms: ${label}`))},ms);using _cleanup={[Symbol.dispose](){clearTimeout(timer)}};return await Promise.race([promise,timeoutPromise])}const DEFAULT_FLUSH_DEADLINE_MS=5e3,DEFAULT_VERIFY_TIMEOUT_MS=5e3,STARTUP_BUFFER_CAP=1e4;async function trackWrite({writePromise}){try{await writePromise}catch(error){reportLoggerInternalError({context:`sink write promise rejected while being tracked`,error})}}function createLogger({sinks,flushDeadlineMs=DEFAULT_FLUSH_DEADLINE_MS,verifyTimeoutMs=DEFAULT_VERIFY_TIMEOUT_MS}){let entries=sinks.map(function(sink){return{available:!1,sink}}),startupRecords=[],pendingWrites=new Set,state={droppedStartupRecords:0,hasAvailableSink:!1,initialized:!1};function bufferStartupRecord({record}){startupRecords.length>=1e4&&(startupRecords.shift(),state.droppedStartupRecords+=1),startupRecords.push(record)}function recomputeAvailability(){state.hasAvailableSink=entries.some(function(entry){return entry.available})}function getSinkEntry({entryIndex}){let entry=entries[entryIndex];if(entry===void 0)throw Error(`Missing logger sink entry at index ${entryIndex}.`);return entry}function markEntryUnavailable({entryIndex}){let entry=getSinkEntry({entryIndex});entry.available=!1,recomputeAvailability()}async function removePendingWriteWhenSettled({trackedWrite}){await trackedWrite,pendingWrites.delete(trackedWrite)}function writeRecordToEntry({entryIndex,record}){try{let trackedWrite=trackWrite({writePromise:getSinkEntry({entryIndex}).sink.write(record)});pendingWrites.add(trackedWrite),removePendingWriteWhenSettled({trackedWrite})}catch(error){reportLoggerInternalError({context:`sink write threw synchronously while dispatching record`,error})}}function replayStartupRecordsToEntry({entryIndex}){startupRecords.forEach(function(record){writeRecordToEntry({entryIndex,record})})}function setEntryAvailability({entryIndex,available}){let entry=getSinkEntry({entryIndex});entry.available=available,recomputeAvailability(),available&&replayStartupRecordsToEntry({entryIndex})}async function verifyAndApply({entryIndex}){try{let entry=getSinkEntry({entryIndex});setEntryAvailability({available:await withTimeout({label:`sink ${entryIndex} verify`,ms:verifyTimeoutMs,promise:entry.sink.verify()}),entryIndex})}catch(error){reportLoggerInternalError({context:`sink verification failed for entry ${entryIndex}`,error}),markEntryUnavailable({entryIndex})}}function emitDroppedStartupMarker(){let dropped=state.droppedStartupRecords;if(dropped===0)return;let marker={level:`warn`,message:`${dropped} startup record${dropped===1?``:`s`} dropped before a backend verified (buffer cap ${STARTUP_BUFFER_CAP})`,timestamp:Date.now()};entries.forEach(function(entry,entryIndex){entry.available&&writeRecordToEntry({entryIndex,record:marker})})}async function initialize(){state.initialized||(await Promise.all(entries.map(function(_entry,entryIndex){return verifyAndApply({entryIndex})})),state.initialized=!0,startupRecords.length=0,emitDroppedStartupMarker())}let initPromise=initialize();async function drainPendingWrites(){let writes=[...pendingWrites];await Promise.all(writes)}function createMethod(level){return function(message){if(!state.hasAvailableSink&&state.initialized)throw Error(`No logging backends available`);let record={level,message,timestamp:Date.now()};state.initialized||bufferStartupRecord({record}),entries.map(function(_entry,entryIndex){return entryIndex}).filter(function(entryIndex){return getSinkEntry({entryIndex}).available}).forEach(function(entryIndex){writeRecordToEntry({entryIndex,record})})}}async function runSinkFlushHooks(){await Promise.all(entries.map(async function(entry,entryIndex){let sinkFlush=entry.sink.flush;if(entry.available&&typeof sinkFlush==`function`)try{await sinkFlush()}catch(error){reportLoggerInternalError({context:`sink flush failed for entry ${entryIndex}`,error}),markEntryUnavailable({entryIndex})}}))}async function drainEverything(){await initPromise,await drainPendingWrites(),await runSinkFlushHooks()}function abandonPendingWrites(){pendingWrites.clear()}async function flushAll(){try{await withTimeout({label:`logger flush`,ms:flushDeadlineMs,promise:drainEverything()})}catch(error){reportLoggerInternalError({context:`flush deadline of ${flushDeadlineMs}ms elapsed; abandoning in-flight sink work`,error}),abandonPendingWrites()}}return{initPromise,logger:{debug:createMethod(`debug`),error:createMethod(`error`),fatal:createMethod(`fatal`),flush:flushAll,info:createMethod(`info`),trace:createMethod(`trace`),warn:createMethod(`warn`)}}}function isNeutralizedControl(codeUnit){return codeUnit<32?codeUnit!==10&&codeUnit!==9:codeUnit===127||codeUnit>=128&&codeUnit<=159}function escapeCodeUnit(codeUnit){return`\\u${codeUnit.toString(16).toUpperCase().padStart(4,`0`)}`}function neutralizeControlCharacters(text){let pieces=[];for(let character of text){let codeUnit=character.charCodeAt(0);pieces.push(isNeutralizedControl(codeUnit)?escapeCodeUnit(codeUnit):character)}return pieces.join(``)}const VERBOSE_UNCOMPUTED=Symbol(`logger:verbose-detection-uncomputed`),SILENT_LEVELS=new Set([`debug`,`trace`]);function detectVerbose(){try{if(typeof process<`u`&&process.env.MONOCHROMATIC_VERBOSE===`true`)return!0}catch(error){reportLoggerInternalError({context:`MONOCHROMATIC_VERBOSE environment probe failed during verbose detection`,error})}try{if(typeof process<`u`&&Array.isArray(process.argv)&&process.argv.includes(`--verbose`))return!0}catch(error){reportLoggerInternalError({context:`process argv probe failed during verbose detection`,error})}try{if(`window`in globalThis)return!0}catch(error){reportLoggerInternalError({context:`window probe failed during verbose detection`,error})}return!1}function isWarnSuppressed(){try{return typeof process<`u`&&process.env.MONOCHROMATIC_WARN===`false`}catch(error){return reportLoggerInternalError({context:`MONOCHROMATIC_WARN environment probe failed during warn suppression detection`,error}),!1}}const LEVEL_TO_CONSOLE_METHOD={debug:`debug`,error:`error`,fatal:`error`,info:`info`,trace:`trace`,warn:`warn`};function formatRecord(record){return`[${record.level}] [${new Date(record.timestamp).toISOString()}] ${neutralizeControlCharacters(record.message)}`}function hasProcessStderr(){try{return typeof process>`u`?!1:typeof process.stderr.write==`function`}catch(error){return reportLoggerInternalError({context:`process stderr availability probe failed`,error}),!1}}function writeDebugRunToProcessStderr(text){try{return hasProcessStderr()?(process.stderr.write(`${text}\n`),!0):!1}catch(error){return reportLoggerInternalError({context:`debug run process stderr write failed`,error}),!1}}function emitRun({records,level}){let text=records.map(function(r){return formatRecord(r)}).join(`
|
|
2
|
+
`);if(!(level===`debug`&&writeDebugRunToProcessStderr(text)))try{let method=LEVEL_TO_CONSOLE_METHOD[level],consoleFn=console[method];typeof consoleFn==`function`&&consoleFn.call(console,text)}catch(error){reportLoggerInternalError({context:`console method call failed while emitting log run`,error})}}function groupRuns(records){return records.reduce(function(runs,record){let tail=runs.at(-1);return tail!==void 0&&tail.level===record.level?(tail.records.push(record),runs):(runs.push({level:record.level,records:[record]}),runs)},[])}function verifyConsole(){try{return typeof console>`u`||typeof(hasProcessStderr()?console.info:console.debug)!=`function`||typeof queueMicrotask!=`function`?Promise.resolve(!1):Promise.resolve(!0)}catch(error){return reportLoggerInternalError({context:`console sink verification failed`,error}),Promise.resolve(!1)}}function createConsoleSink(){let state={buffer:[],scheduled:!1,verboseCache:VERBOSE_UNCOMPUTED};function getVerbose(){let cached=state.verboseCache;if(cached!==VERBOSE_UNCOMPUTED)return cached;let computed=detectVerbose();return state.verboseCache=computed,computed}function flushBuffer(){if(state.scheduled=!1,state.buffer.length===0)return;let records=state.buffer.splice(0);for(let run of groupRuns(records))emitRun({level:run.level,records:run.records})}function write(record){return!getVerbose()&&SILENT_LEVELS.has(record.level)||record.level===`warn`&&isWarnSuppressed()?Promise.resolve():(state.buffer.push(record),state.scheduled||(state.scheduled=!0,queueMicrotask(flushBuffer)),Promise.resolve())}function flush(){return flushBuffer(),Promise.resolve()}return{flush,verify:verifyConsole,write}}function isDigits(text){if(text.length===0)return!1;for(let character of text)if(character<`0`||character>`9`)return!1;return!0}function buildLogKey({stamp,nonce,index}){return`monochromatic.log.${stamp}.${nonce}.${index}`}function parseLogKey(key){if(!key.startsWith(`monochromatic.log.`))return{};let segments=key.slice(18).split(`.`);if(segments.length!==3)return{};let[stampText,nonce,indexText]=segments;return stampText===void 0||nonce===void 0||indexText===void 0||!isDigits(stampText)||nonce.length===0||!isDigits(indexText)?{}:{parsed:{key,stamp:Math.trunc(Number(stampText)),nonce,index:Math.trunc(Number(indexText))}}}function compareLogKeys({first,second}){return first.stamp===second.stamp?first.nonce===second.nonce?first.index-second.index:first.nonce<second.nonce?-1:1:first.stamp-second.stamp}function detectWebStorageRuntime(){return`Deno`in globalThis?`deno`:`Bun`in globalThis?`bun`:typeof process<`u`&&typeof process.versions.node==`string`?`node`:`document`in globalThis?`browser`:`unknown`}const RUNTIME_QUOTA_CHARS$1={browser:5242880,bun:1/0,deno:10477569,node:5242880,unknown:1/0};function detectLocalStorageQuotaChars(){return RUNTIME_QUOTA_CHARS$1[detectWebStorageRuntime()]}const QUOTA_EXCEEDED_NAMES=new Set([`QuotaExceededError`,`NS_ERROR_DOM_QUOTA_REACHED`]);function isQuotaExceededError(error){return typeof error==`object`&&!!error&&`name`in error&&typeof error.name==`string`&"A_EXCEEDED_NAMES.has(error.name)}function createLocalStorageStore(){let runIdentity={stamp:Date.now(),nonce:Math.random().toString(36).slice(2,6).padEnd(4,`0`)},state={lineCounter:0,oldestIndex:0,usedChars:0,reportedFailure:!1,adoptedPrior:!1},prior={entries:[],cursor:0},capChars=detectLocalStorageQuotaChars()/2;function ownKey(index){return buildLogKey({stamp:runIdentity.stamp,nonce:runIdentity.nonce,index})}function adoptPriorEntries(){let total=globalThis.localStorage.length,found=[];for(let slot=0;slot<total;slot++){let key=globalThis.localStorage.key(slot);if(key===null)continue;let{parsed}=parseLogKey(key);if(parsed===void 0||parsed.stamp===runIdentity.stamp&&parsed.nonce===runIdentity.nonce)continue;let value=globalThis.localStorage.getItem(key);value!==null&&found.push({...parsed,chars:value.length})}prior.entries=found.toSorted(function(first,second){return compareLogKeys({first,second})}),state.usedChars+=found.reduce(function(sum,entry){return sum+entry.chars},0)}function hasEvictable(){let priorCount=prior.entries.length;return prior.cursor<priorCount||state.oldestIndex<state.lineCounter}function evictOldestPrior(){let entry=prior.entries[prior.cursor];return entry!==void 0&&(prior.cursor++,globalThis.localStorage.removeItem(entry.key),state.usedChars=Math.max(0,state.usedChars-entry.chars),!0)}function evictOldestOwn(){let key=ownKey(state.oldestIndex),evicted=globalThis.localStorage.getItem(key);globalThis.localStorage.removeItem(key),state.oldestIndex++,evicted!==null&&(state.usedChars=Math.max(0,state.usedChars-evicted.length))}function evictOldest(){evictOldestPrior()||state.oldestIndex<state.lineCounter&&evictOldestOwn()}function persist(batch){state.adoptedPrior||(state.adoptedPrior=!0,adoptPriorEntries());let batchChars=batch.length;for(;hasEvictable()&&state.usedChars+batchChars>capChars;)evictOldest();let maxWriteAttempts=prior.entries.length-prior.cursor+(state.lineCounter-state.oldestIndex)+1;for(let writeAttempt=0;writeAttempt<maxWriteAttempts;writeAttempt++)try{globalThis.localStorage.setItem(ownKey(state.lineCounter),batch),state.lineCounter++,state.usedChars+=batchChars,state.reportedFailure=!1;return}catch(error){if(isQuotaExceededError(error)&&hasEvictable()){evictOldest();continue}state.reportedFailure||=(reportLoggerInternalError({context:`localStorage sink record write failed (repeats suppressed until a write next succeeds)`,error}),!0);return}}return{persist}}const FLUSH_BUFFER_CAP_CHARS=32768,FLUSH_IMMEDIATELY_BY_LEVEL={debug:!1,error:!0,fatal:!0,info:!1,trace:!1,warn:!0};function isUnrefableTimer(timer){return typeof timer!=`object`||!timer||!(`unref`in timer)?!1:typeof timer.unref==`function`}function createRecordBuffer({onFlush}){let entries=[],bufferState={chars:0};function charsWith(serialized){let separatorChars=+(entries.length>0);return bufferState.chars+separatorChars+serialized.length}function drain(){if(bufferState.timer!==void 0&&(globalThis.clearTimeout(bufferState.timer),delete bufferState.timer),entries.length===0)return;let batch=entries.join(`
|
|
3
|
+
`);entries.length=0,bufferState.chars=0,onFlush(batch)}function scheduleDeadlineFlush(){if(bufferState.timer!==void 0)return;let timer=globalThis.setTimeout(drain,250);isUnrefableTimer(timer)&&timer.unref(),bufferState.timer=timer}function add(entry){entries.length>0&&charsWith(entry.serialized)>FLUSH_BUFFER_CAP_CHARS&&drain(),bufferState.chars=charsWith(entry.serialized),entries.push(entry.serialized),FLUSH_IMMEDIATELY_BY_LEVEL[entry.level]||bufferState.chars>=FLUSH_BUFFER_CAP_CHARS?drain():scheduleDeadlineFlush()}return globalThis.addEventListener?.(`pagehide`,drain),globalThis.document?.addEventListener(`visibilitychange`,function(){globalThis.document?.visibilityState===`hidden`&&drain()}),{add,drain}}const NODE_LOCALSTORAGE_FLAG=`--localstorage-file`;function nodeWithoutLocalStorageFile(){if(detectWebStorageRuntime()!==`node`||`document`in globalThis)return!1;let flaggedInExecArgv=process.execArgv.some(function(argument){return argument.startsWith(NODE_LOCALSTORAGE_FLAG)}),flaggedInNodeOptions=(process.env.NODE_OPTIONS??``).includes(NODE_LOCALSTORAGE_FLAG);return!(flaggedInExecArgv||flaggedInNodeOptions)}function verifyLocalStorage(){if(nodeWithoutLocalStorageFile())return Promise.resolve(!1);try{let testKey=`__monochromatic_verify__`,testValue=`test-${Date.now()}`;globalThis.localStorage.setItem(testKey,testValue);let readBack=globalThis.localStorage.getItem(testKey);return globalThis.localStorage.removeItem(testKey),Promise.resolve(readBack===testValue)}catch(error){return`localStorage`in globalThis&&reportLoggerInternalError({context:`localStorage sink verification failed`,error}),Promise.resolve(!1)}}function createLocalStorageSink(){let buffer=createRecordBuffer({onFlush:createLocalStorageStore().persist});function write(record){return buffer.add({level:record.level,serialized:JSON.stringify(record)}),Promise.resolve()}function flush(){return buffer.drain(),Promise.resolve()}return{flush,verify:verifyLocalStorage,write}}const RUNTIME_QUOTA_CHARS={browser:5242880,bun:1/0,deno:10485760,node:5242880,unknown:1/0};function detectSessionStorageQuotaChars(){return RUNTIME_QUOTA_CHARS[detectWebStorageRuntime()]}function storageKey(index){return`monochromatic.log.${index}`}function createSessionStorageStore(){let state={lineCounter:0,oldestIndex:0,usedChars:0,reportedFailure:!1},capChars=detectSessionStorageQuotaChars()/2;function evictOldest(){let key=storageKey(state.oldestIndex),evicted=globalThis.sessionStorage.getItem(key);globalThis.sessionStorage.removeItem(key),state.oldestIndex++,evicted!==null&&(state.usedChars=Math.max(0,state.usedChars-evicted.length))}function persist(batch){let batchChars=batch.length;for(;state.oldestIndex<state.lineCounter&&state.usedChars+batchChars>capChars;)evictOldest();let maxWriteAttempts=state.lineCounter-state.oldestIndex+1;for(let writeAttempt=0;writeAttempt<maxWriteAttempts;writeAttempt++)try{globalThis.sessionStorage.setItem(storageKey(state.lineCounter),batch),state.lineCounter++,state.usedChars+=batchChars,state.reportedFailure=!1;return}catch(error){if(isQuotaExceededError(error)&&state.oldestIndex<state.lineCounter){evictOldest();continue}state.reportedFailure||=(reportLoggerInternalError({context:`sessionStorage sink record write failed (repeats suppressed until a write next succeeds)`,error}),!0);return}}return{persist}}function verifySessionStorage(){try{let testKey=`__monochromatic_verify__`,testValue=`test-${Date.now()}`;globalThis.sessionStorage.setItem(testKey,testValue);let readBack=globalThis.sessionStorage.getItem(testKey);return globalThis.sessionStorage.removeItem(testKey),Promise.resolve(readBack===testValue)}catch(error){return`sessionStorage`in globalThis&&reportLoggerInternalError({context:`sessionStorage sink verification failed`,error}),Promise.resolve(!1)}}function createSessionStorageSink(){let buffer=createRecordBuffer({onFlush:createSessionStorageStore().persist});function write(record){return buffer.add({level:record.level,serialized:JSON.stringify(record)}),Promise.resolve()}function flush(){return buffer.drain(),Promise.resolve()}return{flush,verify:verifySessionStorage,write}}const{initPromise:defaultInitPromise,logger:defaultLogger}=createLogger({sinks:[createConsoleSink(),createSessionStorageSink(),createLocalStorageSink(),createFileSink()]}),initPromise=defaultInitPromise,logger=defaultLogger;function verify(){return Promise.resolve(!0)}function write(_record){return Promise.resolve()}function createNoopSink(){return{verify,write}}var sink_exports=__exportAll({createConsoleSink:()=>createConsoleSink,createLocalStorageSink:()=>createLocalStorageSink,createNoopSink:()=>createNoopSink,createSessionStorageSink:()=>createSessionStorageSink});function tagged({tag,l=logger}){let prefix=`[${tag}] `;return{debug:function(message){l.debug(`${prefix}${message}`)},error:function(message){l.error(`${prefix}${message}`)},fatal:function(message){l.fatal(`${prefix}${message}`)},flush:function(){return l.flush()},info:function(message){l.info(`${prefix}${message}`)},trace:function(message){l.trace(`${prefix}${message}`)},warn:function(message){l.warn(`${prefix}${message}`)}}}export{DEFAULT_FLUSH_DEADLINE_MS,DEFAULT_VERIFY_TIMEOUT_MS,STARTUP_BUFFER_CAP,buildLogKey as _buildLogKey,compareLogKeys as _compareLogKeys,createLocalStorageStore as _createLocalStorageStore,createRecordBuffer as _createRecordBuffer,detectLocalStorageQuotaChars as _detectLocalStorageQuotaChars,detectSessionStorageQuotaChars as _detectSessionStorageQuotaChars,isQuotaExceededError as _isQuotaExceededError,neutralizeControlCharacters as _neutralizeControlCharacters,parseLogKey as _parseLogKey,createLogger,initPromise,logger,sink_exports as sinks,tagged};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { i as Sink } from "./types-BkkBXgY3.mjs";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
//#region src/error-format.d.ts
|
|
5
|
+
/**
|
|
6
|
+
Reports a logger-internal caught value without going back through logger
|
|
7
|
+
sinks, formatting it via {@link caughtValueText}.
|
|
8
|
+
|
|
9
|
+
@param context - Human-readable operation that caught the value.
|
|
10
|
+
|
|
11
|
+
@param error - Caught value to include in the diagnostic.
|
|
12
|
+
|
|
13
|
+
@mutates error - `caughtValueText` may invoke string-conversion hooks.
|
|
14
|
+
|
|
15
|
+
@example
|
|
16
|
+
```ts
|
|
17
|
+
reportLoggerInternalError({
|
|
18
|
+
context: 'console sink verify failed',
|
|
19
|
+
error: new Error('blocked'),
|
|
20
|
+
});
|
|
21
|
+
```
|
|
22
|
+
*/
|
|
23
|
+
declare function reportLoggerInternalError({ context, error }: {
|
|
24
|
+
readonly context: string;
|
|
25
|
+
readonly error: unknown;
|
|
26
|
+
}): void;
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/sink/file.d.ts
|
|
29
|
+
/**
|
|
30
|
+
Sentinel returned by {@link findNodeModulesUp} when no ancestor directory
|
|
31
|
+
contains a `node_modules`. A unique symbol so it never collides with a real
|
|
32
|
+
path string the walk might otherwise return, keeping the result free of a
|
|
33
|
+
banned `string | undefined` union.
|
|
34
|
+
|
|
35
|
+
@example
|
|
36
|
+
```ts
|
|
37
|
+
const dir = await findNodeModulesUp({ cwd, stat, dirname, join });
|
|
38
|
+
if (dir === NO_NODE_MODULES_FOUND) {
|
|
39
|
+
// no ancestor project root
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
*/
|
|
43
|
+
declare const NO_NODE_MODULES_FOUND: unique symbol;
|
|
44
|
+
/**
|
|
45
|
+
Walks up from `cwd` to find the nearest ancestor directory containing a
|
|
46
|
+
`node_modules` subdirectory, returning that subdirectory's absolute path.
|
|
47
|
+
|
|
48
|
+
Using find-up rather than cwd-relative placement keeps log directories
|
|
49
|
+
anchored to the project the caller actually belongs to. Without this,
|
|
50
|
+
scripts invoked from build output (e.g. `dist/`) or other stray cwds
|
|
51
|
+
would create `node_modules/.monochromatic/` inside those trees, polluting
|
|
52
|
+
shipped artifacts.
|
|
53
|
+
|
|
54
|
+
Exported through the `./node` subpath primarily so `file.unit.test.ts`
|
|
55
|
+
can exercise both the hit and miss paths directly with an injected `stat`.
|
|
56
|
+
|
|
57
|
+
@param cwd - starting directory for the upward search
|
|
58
|
+
|
|
59
|
+
@param stat - `node:fs/promises` stat, injected so tests drive hit,
|
|
60
|
+
miss, and fault paths deterministically without touching a real tree
|
|
61
|
+
|
|
62
|
+
@param dirname - `node:path` dirname
|
|
63
|
+
|
|
64
|
+
@param join - `node:path` join
|
|
65
|
+
|
|
66
|
+
@param reportError - logger fault reporter injected for deterministic tests
|
|
67
|
+
|
|
68
|
+
@returns absolute path to the nearest ancestor `node_modules`, or
|
|
69
|
+
{@link NO_NODE_MODULES_FOUND} when no ancestor contains one
|
|
70
|
+
|
|
71
|
+
@example
|
|
72
|
+
```ts
|
|
73
|
+
const dir = await findNodeModulesUp({ cwd: process.cwd(), stat, dirname, join });
|
|
74
|
+
```
|
|
75
|
+
*/
|
|
76
|
+
declare function findNodeModulesUp({ cwd, stat: statPath, dirname: parentOf, join: joinPath, reportError }: {
|
|
77
|
+
readonly cwd: string;
|
|
78
|
+
readonly stat: typeof stat;
|
|
79
|
+
readonly dirname: typeof dirname;
|
|
80
|
+
readonly join: typeof join;
|
|
81
|
+
readonly reportError?: typeof reportLoggerInternalError;
|
|
82
|
+
}): Promise<string | typeof NO_NODE_MODULES_FOUND>;
|
|
83
|
+
/**
|
|
84
|
+
Builds a file sink that appends JSONL records to the nearest ancestor
|
|
85
|
+
`node_modules/.monochromatic/{timestamp}.log.jsonl` (resolved once during
|
|
86
|
+
verification). The resolved path and the verification memo live in this
|
|
87
|
+
instance's closure (no module-global state), so independent loggers and
|
|
88
|
+
tests never share a log file or need a reset hook. No `flush` hook: each
|
|
89
|
+
`write` awaits `appendFile` directly, so there is no buffered state to
|
|
90
|
+
drain.
|
|
91
|
+
|
|
92
|
+
@returns Sink backed by `node:fs/promises`.
|
|
93
|
+
|
|
94
|
+
@example
|
|
95
|
+
```ts
|
|
96
|
+
const { logger } = createLogger({ sinks: [createFileSink()] });
|
|
97
|
+
logger.error('unhandled rejection');
|
|
98
|
+
await logger.flush();
|
|
99
|
+
```
|
|
100
|
+
*/
|
|
101
|
+
export declare function createFileSink(): Sink;
|
|
102
|
+
//#endregion
|
|
103
|
+
export { NO_NODE_MODULES_FOUND as _NO_NODE_MODULES_FOUND, findNodeModulesUp as _findNodeModulesUp };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{n as createFileSink,r as findNodeModulesUp,t as NO_NODE_MODULES_FOUND}from"./file-CRGb1hDK.mjs";export{NO_NODE_MODULES_FOUND as _NO_NODE_MODULES_FOUND,findNodeModulesUp as _findNodeModulesUp,createFileSink};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
Log severity levels ordered from least to most severe.
|
|
4
|
+
*/
|
|
5
|
+
type Level = "debug" | "error" | "fatal" | "info" | "trace" | "warn";
|
|
6
|
+
/**
|
|
7
|
+
Structured log record written to sinks.
|
|
8
|
+
*/
|
|
9
|
+
type LogRecord = {
|
|
10
|
+
readonly level: Level;
|
|
11
|
+
readonly message: string;
|
|
12
|
+
readonly timestamp: number;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
Optional drain hook for sinks that buffer records internally.
|
|
16
|
+
Called via logger-level {@link Logger.flush} to force buffered work
|
|
17
|
+
through before a process exit, critical error boundary, or assertion.
|
|
18
|
+
|
|
19
|
+
Always async: sinks whose drain is synchronous return an
|
|
20
|
+
already-resolved promise so callers `await` uniformly. A `void` arm is
|
|
21
|
+
not used; under the `no-optional-escape` rule `T | void` is a banned
|
|
22
|
+
fake-optional encoding, and there is no real synchronous value to carry.
|
|
23
|
+
*/
|
|
24
|
+
type SinkFlush = () => Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
Verification function that checks if a sink backend is available.
|
|
27
|
+
May run setup side effects (resolving a log path, opening a writable
|
|
28
|
+
stream) and reports whether the backend is usable. A sink whose
|
|
29
|
+
verification resolves `false` (or rejects) is dropped by the logger and
|
|
30
|
+
receives no further records.
|
|
31
|
+
|
|
32
|
+
Always async, matching `write` and `flush`: a synchronous check returns an
|
|
33
|
+
already-resolved promise (`Promise.resolve(check)`) so the logger awaits
|
|
34
|
+
verification uniformly with no sync/async branch.
|
|
35
|
+
*/
|
|
36
|
+
type Verify = () => Promise<boolean>;
|
|
37
|
+
/**
|
|
38
|
+
Sink that receives log records. A sink is a self-describing adapter: it
|
|
39
|
+
carries everything the logger must know to use it, namely how to
|
|
40
|
+
`verify` its backend is available, how to `write` a record, and
|
|
41
|
+
optionally how to `flush` buffered work. Holding `verify` on the sink
|
|
42
|
+
(rather than as a sibling export the logger pairs by hand) lets the
|
|
43
|
+
logger treat a registry as a plain `Sink[]` and lets a test supply one
|
|
44
|
+
self-contained fake.
|
|
45
|
+
|
|
46
|
+
Sinks that buffer records (e.g. microtask-batched console) may
|
|
47
|
+
expose a `flush` hook so callers can force emission on demand.
|
|
48
|
+
|
|
49
|
+
`write` is always async: a synchronous sink does its work eagerly and
|
|
50
|
+
returns an already-resolved promise, so the logger observes a uniform
|
|
51
|
+
`Promise<void>`. A rejected write is handled per sink and does not
|
|
52
|
+
disable the backend; only a failed `verify` drops a sink. A `void` arm
|
|
53
|
+
is not used, for the reason stated on {@link SinkFlush}.
|
|
54
|
+
*/
|
|
55
|
+
type Sink = {
|
|
56
|
+
readonly flush?: SinkFlush;
|
|
57
|
+
readonly verify: Verify;
|
|
58
|
+
readonly write: (record: LogRecord) => Promise<void>;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
Logger interface with 6 log levels plus `flush` for startup and sink drains.
|
|
62
|
+
`flush()` resolves once startup verification has completed, tracked sink
|
|
63
|
+
writes have settled, and every available sink's own {@link SinkFlush} hook
|
|
64
|
+
has settled. Safe to call even when no sink buffers.
|
|
65
|
+
*/
|
|
66
|
+
type Logger = {
|
|
67
|
+
readonly debug: (message: string) => void;
|
|
68
|
+
readonly error: (message: string) => void;
|
|
69
|
+
readonly fatal: (message: string) => void;
|
|
70
|
+
readonly flush: () => Promise<void>;
|
|
71
|
+
readonly info: (message: string) => void;
|
|
72
|
+
readonly trace: (message: string) => void;
|
|
73
|
+
readonly warn: (message: string) => void;
|
|
74
|
+
};
|
|
75
|
+
//#endregion
|
|
76
|
+
export { SinkFlush as a, Sink as i, LogRecord as n, Verify as o, Logger as r, Level as t };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"license": "LGPL-3.0-or-later",
|
|
3
3
|
"name": "@monochromatic-dev/module-logger",
|
|
4
|
-
"description": "Zero-config multi-sink logger with tagged composition. Auto-discovers console,
|
|
4
|
+
"description": "Zero-config multi-sink logger with tagged composition. Auto-discovers console, sessionStorage, and localStorage backends at import time, plus the file backend under Node and IndexedDB in browsers, with no dynamic imports in either build.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "dist/final/neutral/index.mjs",
|
|
7
7
|
"exports": {
|
|
@@ -9,6 +9,20 @@
|
|
|
9
9
|
"types": "./dist/final/neutral/index.d.mts",
|
|
10
10
|
"node": "./dist/final/node/index.mjs",
|
|
11
11
|
"default": "./dist/final/neutral/index.mjs"
|
|
12
|
+
},
|
|
13
|
+
"./node": {
|
|
14
|
+
"types": "./dist/final/node/node.d.mts",
|
|
15
|
+
"default": "./dist/final/node/node.mjs"
|
|
16
|
+
},
|
|
17
|
+
"./browser": {
|
|
18
|
+
"types": "./dist/final/neutral/browser.d.mts",
|
|
19
|
+
"default": "./dist/final/neutral/browser.mjs"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"imports": {
|
|
23
|
+
"#default-sinks": {
|
|
24
|
+
"node": "./src/default-sinks.node.ts",
|
|
25
|
+
"default": "./src/default-sinks.neutral.ts"
|
|
12
26
|
}
|
|
13
27
|
},
|
|
14
28
|
"files": [
|
|
@@ -23,11 +37,11 @@
|
|
|
23
37
|
"@monochromatic-dev/config-rolldown": "0.0.1",
|
|
24
38
|
"@monochromatic-dev/module-async-time": "0.0.1",
|
|
25
39
|
"@monochromatic-dev/module-test": "0.0.1",
|
|
26
|
-
"@monochromatic-dev/
|
|
27
|
-
"@monochromatic-dev/
|
|
40
|
+
"@monochromatic-dev/config-typescript": "0.0.5",
|
|
41
|
+
"@monochromatic-dev/module-caught-value": "0.0.1"
|
|
28
42
|
},
|
|
29
43
|
"dependencies": {},
|
|
30
|
-
"version": "0.
|
|
44
|
+
"version": "0.3.0",
|
|
31
45
|
"repository": {
|
|
32
46
|
"type": "git",
|
|
33
47
|
"url": "https://github.com/Aquaticat/Monochromatic.git",
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readdir,
|
|
3
|
+
readFile,
|
|
4
|
+
} from 'node:fs/promises';
|
|
5
|
+
import { join, } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
describe,
|
|
9
|
+
expect,
|
|
10
|
+
it,
|
|
11
|
+
} from '@monochromatic-dev/module-test/ts';
|
|
12
|
+
|
|
13
|
+
// Guards the platform split of the two built artifacts. Every `.mjs` file in
|
|
14
|
+
// a build directory is read (named entries plus hashed shared chunks) and
|
|
15
|
+
// concatenated, so an assertion about "the node build" covers whatever
|
|
16
|
+
// rolldown hoisted out of the entries. Positive controls come first: a probe
|
|
17
|
+
// that cannot see `createFileSink` in the node build proves nothing about its
|
|
18
|
+
// absence elsewhere.
|
|
19
|
+
|
|
20
|
+
//region Build text
|
|
21
|
+
/**
|
|
22
|
+
Directory holding both platform builds, resolved from this test file so
|
|
23
|
+
the guard reads whatever the last `build` task emitted.
|
|
24
|
+
*/
|
|
25
|
+
const distFinalDir = join(
|
|
26
|
+
import.meta.dirname,
|
|
27
|
+
'..',
|
|
28
|
+
'dist',
|
|
29
|
+
'final',
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
Reads every `.mjs` file in one build directory and joins their text.
|
|
34
|
+
|
|
35
|
+
@param build - Build directory name under `dist/final`.
|
|
36
|
+
|
|
37
|
+
@returns Concatenated text of every `.mjs` file in that build.
|
|
38
|
+
|
|
39
|
+
@throws Error when the directory holds no `.mjs` file, which means the
|
|
40
|
+
build has not run and every negative assertion would pass vacuously.
|
|
41
|
+
|
|
42
|
+
@example
|
|
43
|
+
```ts
|
|
44
|
+
const text = await buildText({ build: 'node' });
|
|
45
|
+
```
|
|
46
|
+
*/
|
|
47
|
+
async function buildText({ build, }: { readonly build: 'neutral' | 'node'; },): Promise<string> {
|
|
48
|
+
/**
|
|
49
|
+
Build directory whose modules are concatenated.
|
|
50
|
+
*/
|
|
51
|
+
const dir = join(
|
|
52
|
+
distFinalDir,
|
|
53
|
+
build,
|
|
54
|
+
);
|
|
55
|
+
/**
|
|
56
|
+
Module file names in the build, chunks included; declaration files are skipped.
|
|
57
|
+
*/
|
|
58
|
+
const names = (await readdir(dir,)).filter(function isModule(name: string,): boolean {
|
|
59
|
+
return name.endsWith('.mjs',);
|
|
60
|
+
},);
|
|
61
|
+
if (names.length === 0)
|
|
62
|
+
throw new Error(`no .mjs files in ${dir}; run the build before this guard`,);
|
|
63
|
+
/**
|
|
64
|
+
Module texts in directory order.
|
|
65
|
+
*/
|
|
66
|
+
const texts = await Promise.all(names.map(async function readModule(name: string,): Promise<string> {
|
|
67
|
+
return await readFile(
|
|
68
|
+
join(
|
|
69
|
+
dir,
|
|
70
|
+
name,
|
|
71
|
+
),
|
|
72
|
+
'utf8',
|
|
73
|
+
);
|
|
74
|
+
},),);
|
|
75
|
+
return texts.join('\n',);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
Concatenated text of the Node artifact (`dist/final/node/*.mjs`).
|
|
80
|
+
*/
|
|
81
|
+
const nodeText = await buildText({ build: 'node', },);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
Concatenated text of the platform-neutral artifact (`dist/final/neutral/*.mjs`).
|
|
85
|
+
*/
|
|
86
|
+
const neutralText = await buildText({ build: 'neutral', },);
|
|
87
|
+
//endregion Build text
|
|
88
|
+
|
|
89
|
+
await describe({
|
|
90
|
+
name: 'artifact platform split',
|
|
91
|
+
children: [
|
|
92
|
+
it({
|
|
93
|
+
name: 'positive control: node build ships the file sink with a static node:fs/promises import',
|
|
94
|
+
fn: async () => {
|
|
95
|
+
expect(nodeText.includes('createFileSink',),).toBe(true,);
|
|
96
|
+
expect(nodeText.includes('node:fs/promises',),).toBe(true,);
|
|
97
|
+
},
|
|
98
|
+
},),
|
|
99
|
+
|
|
100
|
+
it({
|
|
101
|
+
name: 'positive control: neutral build ships the IndexedDB sink',
|
|
102
|
+
fn: async () => {
|
|
103
|
+
expect(neutralText.includes('createIndexedDbSink',),).toBe(true,);
|
|
104
|
+
},
|
|
105
|
+
},),
|
|
106
|
+
|
|
107
|
+
it({
|
|
108
|
+
name: 'neither build contains a dynamic import',
|
|
109
|
+
fn: async () => {
|
|
110
|
+
expect(nodeText.includes('import(',),).toBe(false,);
|
|
111
|
+
expect(neutralText.includes('import(',),).toBe(false,);
|
|
112
|
+
},
|
|
113
|
+
},),
|
|
114
|
+
|
|
115
|
+
it({
|
|
116
|
+
name: 'neutral build names neither node:fs nor node:path',
|
|
117
|
+
fn: async () => {
|
|
118
|
+
expect(neutralText.includes('node:fs',),).toBe(false,);
|
|
119
|
+
expect(neutralText.includes('node:path',),).toBe(false,);
|
|
120
|
+
},
|
|
121
|
+
},),
|
|
122
|
+
|
|
123
|
+
it({
|
|
124
|
+
name: 'node build touches neither indexedDB nor navigator.storage',
|
|
125
|
+
fn: async () => {
|
|
126
|
+
expect(nodeText.includes('indexedDB',),).toBe(false,);
|
|
127
|
+
expect(nodeText.includes('navigator.storage',),).toBe(false,);
|
|
128
|
+
},
|
|
129
|
+
},),
|
|
130
|
+
|
|
131
|
+
it({
|
|
132
|
+
name: 'platform-specific factories stay on their own side',
|
|
133
|
+
fn: async () => {
|
|
134
|
+
expect(neutralText.includes('createFileSink',),).toBe(false,);
|
|
135
|
+
expect(nodeText.includes('createIndexedDbSink',),).toBe(false,);
|
|
136
|
+
expect(nodeText.includes('createOpfsSink',),).toBe(false,);
|
|
137
|
+
},
|
|
138
|
+
},),
|
|
139
|
+
],
|
|
140
|
+
},);
|
package/src/browser.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Browser-only entry (`\@monochromatic-dev/module-logger/browser`).
|
|
3
|
+
|
|
4
|
+
Ships the sinks whose backends exist only in browsers (`indexedDB`,
|
|
5
|
+
`navigator.storage`). Built only by `rolldown.browser.config.ts`, so the
|
|
6
|
+
Node artifact carries no dead browser-storage probes. The IndexedDB sink is
|
|
7
|
+
also part of the neutral default list (see `default-sinks.neutral.ts`);
|
|
8
|
+
the OPFS sink is opt-in only, see `DECISIONS.md`.
|
|
9
|
+
|
|
10
|
+
@module
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export { createIndexedDbSink, } from './sink/indexed-db.ts';
|
|
14
|
+
export { createOpfsSink, } from './sink/opfs.ts';
|