@socketsupply/socket
Version:
A Cross-Platform, Native Runtime for Desktop and Mobile Apps — Create apps using HTML, CSS, and JavaScript. Written from the ground up to be small and maintainable.
440 lines (373 loc) • 10.8 kB
JavaScript
import { format, isObject } from './util.js'
import { postMessage } from './ipc.js'
import os from './os.js'
function isPatched (console) {
return console?.[Symbol.for('socket.runtime.console.patched')] === true
}
function table (data, columns, formatValues = true) {
const maybeFormat = (value) => formatValues ? format(value) : value
const rows = []
columns = Array.isArray(columns) && Array.from(columns)
if (!columns) {
columns = []
if (Array.isArray(data)) {
const first = data[0]
if (!isObject(first) && first !== undefined) {
columns.push('Values')
} else if (isObject(first)) {
const keys = new Set(data
.map(Object.keys)
.reduce((a, b) => a.concat(b), [])
)
columns.push(...keys.values())
}
} else if (isObject(data)) {
columns.push('Value')
for (const key in data) {
const value = data[key]
if (isObject(value)) {
columns.push(key)
}
}
}
}
if (Array.isArray(data)) {
const first = data[0]
if (!isObject(first) && first !== undefined) {
for (let i = 0; i < data.length; ++i) {
const value = maybeFormat(data[i] ?? null)
rows.push([i, value])
}
} else {
for (let i = 0; i < data.length; ++i) {
const row = [i]
for (const column of columns) {
if (column === 'Value' && !isObject(data[i])) {
const value = maybeFormat(data[i] ?? null)
row.push(value ?? null)
} else {
const value = maybeFormat(data[i]?.[column] ?? data[i] ?? null)
row.push(value)
}
}
rows.push(row)
}
}
} else if (isObject(data)) {
for (const key in data) {
rows.push([key, maybeFormat(data[key] ?? null)])
}
}
columns.unshift('(index)')
return { columns, rows }
}
export const globalConsole = globalThis?.console ?? null
export class Console {
/**
* @type {import('dom').Console}
*/
console = null
/**
* @type {Map}
*/
timers = new Map()
/**
* @type {Map}
*/
counters = new Map()
/**
* @type {function?}
*/
postMessage = null
/**
* @ignore
*/
constructor (options) {
if (typeof options?.postMessage !== 'function') {
throw new TypeError('Expecting `.postMessage` in constructor options')
}
Object.defineProperties(this, {
postMessage: {
...Object.getOwnPropertyDescriptor(this, 'postMessage'),
enumerable: false,
configurable: false,
value: options?.postMessage
},
console: {
...Object.getOwnPropertyDescriptor(this, 'console'),
enumerable: false,
configurable: false,
value: options?.console ?? null
},
counters: {
...Object.getOwnPropertyDescriptor(this, 'counters'),
enumerable: false,
configurable: false
},
timers: {
...Object.getOwnPropertyDescriptor(this, 'timers'),
enumerable: false,
configurable: false
}
})
this.write = this.write.bind(this)
this.assert = this.assert.bind(this)
this.clear = this.clear.bind(this)
this.count = this.count.bind(this)
this.countReset = this.countReset.bind(this)
this.debug = this.debug.bind(this)
this.dir = this.dir.bind(this)
this.dirxml = this.dirxml.bind(this)
this.error = this.error.bind(this)
this.info = this.info.bind(this)
this.log = this.log.bind(this)
this.table = this.table.bind(this)
this.time = this.time.bind(this)
this.timeEnd = this.timeEnd.bind(this)
this.timeLog = this.timeLog.bind(this)
this.trace = this.trace.bind(this)
this.warn = this.warn.bind(this)
}
write (destination, ...args) {
let extra = ''
let value = ''
value = format(...args)
if (destination === 'debug') {
destination = 'stderr'
extra = 'debug=true'
if (globalThis.location && !globalThis.window) {
value = `[${globalThis.name || globalThis.location.pathname}]: ${value}`
} else {
value = `[${globalThis.location.pathname}]: ${value}`
}
}
if (/ios|darwin/i.test(os.platform())) {
const parts = value.split('\n')
const pending = []
for (const part of parts) {
if (part.length > 256) {
for (let i = 0; i < part.length; i += 256) {
pending.push(part.slice(i, i + 256))
}
} else {
pending.push(part)
}
while (pending.length) {
const output = pending.shift()
try {
const value = encodeURIComponent(output)
const uri = `ipc://${destination}?value=${value}&${extra ? extra + '&' : ''}resolve=false`
this.postMessage?.(uri)
} catch (err) {
this.console?.warn?.(`Failed to write to ${destination}: ${err.message}`)
return
}
}
}
return
}
value = encodeURIComponent(value)
const uri = `ipc://${destination}?value=${value}&${extra}&resolve=false`
try {
return this.postMessage?.(uri)
} catch (err) {
this.console?.warn?.(`Failed to write to ${destination}: ${err.message}`)
}
}
assert (assertion, ...args) {
this.console?.assert?.(assertion, ...args)
if (Boolean(assertion) !== true) {
this.write('stderr', `Assertion failed: ${format(...args)}`)
}
}
clear () {
this.console?.clear?.()
}
count (label = 'default') {
this.console?.count(label)
if (!isPatched(this.console)) {
const count = (this.counters.get(label) || 0) + 1
this.counters.set(label, count)
this.write('stdout', `${label}: ${count}`)
}
}
countReset (label = 'default') {
this.console?.countReset()
if (!isPatched(this.console)) {
this.counters.set(label, 0)
this.write('stdout', `${label}: 0`)
}
}
debug (...args) {
this.console?.debug?.(...args)
if (!isPatched(this.console)) {
this.write('debug', ...args)
}
}
dir (...args) {
this.console?.dir?.(...args)
}
dirxml (...args) {
this.console?.dirxml?.(...args)
}
error (...args) {
this.console?.error?.(...args)
if (!isPatched(this.console)) {
this.write('stderr', ...args)
}
}
info (...args) {
this.console?.info?.(...args)
if (!isPatched(this.console)) {
this.write('stdout', ...args)
}
}
log (...args) {
this.console?.log?.(...args)
if (!isPatched(this.console)) {
this.write('stdout', ...args)
}
}
table (...args) {
if (isPatched(this.console)) {
return this.console.table(...args)
}
if (!isObject(args[0])) {
return this.log(...args)
}
// @ts-ignore
const { columns, rows } = table(...args)
const output = []
const widths = []
for (let i = 0; i < columns.length; ++i) {
const column = columns[i]
let columnWidth = column.length + 2
for (const row of rows) {
const cell = row[i]
const cellContents = String(cell)
const cellWidth = 2 + (cellContents
.split('\n')
.map((r) => r.length)
.sort()
.slice(-1)[0] ?? 0
)
columnWidth = Math.max(columnWidth, cellWidth)
}
columnWidth += 2
widths.push(columnWidth)
output.push(column.padEnd(columnWidth, ' '))
}
output.push('\n')
for (let i = 0; i < rows.length; ++i) {
const row = rows[i]
for (let j = 0; j < row.length; ++j) {
const width = widths[j]
const cell = String(row[j])
output.push(cell.padEnd(width, ' '))
}
output.push('\n')
}
this.write('stdout', output.join(''))
this.console?.table?.(...args)
}
time (label = 'default') {
this.console?.time?.(label)
if (!isPatched(this.console)) {
if (this.timers.has(label)) {
this.console?.warn?.(
`Warning: Label '${label}' already exists for console.time()`
)
} else {
this.timers.set(label, Date.now())
}
}
}
timeEnd (label = 'default') {
this.console?.timeEnd?.(label)
if (!isPatched(this.console)) {
if (!this.timers.has(label)) {
this.console?.warn?.(
`Warning: No such label '${label}' for console.timeEnd()`
)
} else {
const time = this.timers.get(label)
this.timers.delete(label)
if (typeof time === 'number' && time >= 0) {
const elapsed = Date.now() - time
if (elapsed >= 1000) {
this.write('stdout', `${label}: ${elapsed * 0.001}s`)
} else {
this.write('stdout', `${label}: ${elapsed}ms`)
}
}
}
}
}
timeLog (label = 'default') {
this.console?.timeLog?.(label)
if (!isPatched(this.console)) {
if (!this.timers.has(label)) {
this.console?.warn?.(
`Warning: No such label '${label}' for console.timeLog()`
)
} else {
const time = this.timers.get(label)
if (typeof time === 'number' && time >= 0) {
const elapsed = Date.now() - time
if (elapsed * 0.001 > 0) {
this.write('stdout', `${label}: ${elapsed * 0.001}s`)
} else {
this.write('stdout', `${label}: ${elapsed}ms`)
}
}
}
}
}
trace (...objects) {
this.console?.trace?.(...objects)
if (!isPatched(this.console)) {
const stack = new Error().stack.split('\n').slice(1)
stack.unshift(`Trace: ${format(...objects)}`)
this.write('stderr', stack.join('\n'))
}
}
warn (...args) {
this.console?.warn?.(...args)
if (!isPatched(this.console)) {
this.write('stderr', ...args)
}
}
}
export function patchGlobalConsole (globalConsole, options = {}) {
if (!globalConsole || typeof globalConsole !== 'object') {
globalConsole = globalThis?.console
}
if (!globalConsole) {
throw new TypeError('Cannot determine a global console object to patch')
}
if (!isPatched(globalConsole)) {
const defaultConsole = new Console({
postMessage,
...options
})
globalConsole[Symbol.for('socket.runtime.console.patched')] = true
for (const key in globalConsole) {
if (typeof Console.prototype[key] === 'function') {
const original = globalConsole[key].bind(globalConsole)
globalConsole[key] = function (...args) {
original(...args)
defaultConsole[key](...args)
}
globalConsole[key].platform = original
}
}
}
return globalConsole
}
export default Object.assign(new Console({
postMessage,
console: patchGlobalConsole(globalConsole)
}), {
Console,
globalConsole
})