mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-23 05:41:20 -08:00
refactor(site): [SITE-CONSOLE-01] restore typed console and lifecycle ownership
This commit is contained in:
1 parent
6cd2d0ffbe
commit
3dba82a453
29 files changed
+1884
-36
No files matched your search
@@ -0,0 +1,10 @@
|
||||
import type { DomRuntime, ReactRuntime } from './types.ts'
|
||||
|
||||
export function install(React: ReactRuntime, ReactDOM: DomRuntime, View: ReactRuntime['Component'], host: Record<string, unknown>) {
|
||||
const consoleLog = (element: Element) => ReactDOM.render(React.createElement(View), element)
|
||||
consoleLog.unmount = (element: Element) => ReactDOM.unmountComponentAtNode(element)
|
||||
host.React = React
|
||||
host.ReactDOM = ReactDOM
|
||||
host.consoleLog = consoleLog
|
||||
return consoleLog
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export function errorArgument(value: unknown): unknown {
|
||||
try {
|
||||
if (Object.prototype.toString.call(value) !== '[object Error]')
|
||||
return value
|
||||
const error = value as Error
|
||||
const stack = error.stack
|
||||
const message = error.message
|
||||
if (typeof stack !== 'string' || !message)
|
||||
return value
|
||||
const header = error.name ? `${error.name}: ${message}` : message
|
||||
return stack.startsWith(header) ? value : `${header}\n${stack}`
|
||||
}
|
||||
catch {
|
||||
// Keep the legacy parser's behavior for unusual throwing Error getters.
|
||||
return value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
export const css = `
|
||||
position: relative;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #242424;
|
||||
.console-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: #eee;
|
||||
padding: 7px 10px;
|
||||
font-size: 12px;
|
||||
background-color: #111;
|
||||
border-bottom: 1px solid rgb(255 255 255 / 10%);
|
||||
user-select: none;
|
||||
.console-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.console-header-number {
|
||||
margin-left: 5px;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
}
|
||||
.console-header-right {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.console-component {
|
||||
scroll-behavior: smooth;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
height: 200px;
|
||||
flex: 1;
|
||||
}
|
||||
`
|
||||
@@ -0,0 +1,107 @@
|
||||
import type { Clock, ConsoleTarget, Log, NativeMethod, Parser } from './types.ts'
|
||||
import { errorArgument } from './errors.ts'
|
||||
|
||||
interface Listener { callback?: (log: Log) => void }
|
||||
interface SavedMethod { name: string, descriptor?: PropertyDescriptor, wrapper: NativeMethod }
|
||||
interface Owner {
|
||||
listeners: Set<Listener>
|
||||
pending: Set<number>
|
||||
saved: SavedMethod[]
|
||||
feed: { pointers: Record<string, unknown>, src: { npm: string, github: string } }
|
||||
previousFeed?: PropertyDescriptor
|
||||
}
|
||||
|
||||
export function createSubscriptions(target: ConsoleTarget, methods: string[], parse: Parser, clock: Clock) {
|
||||
let owner: Owner | undefined
|
||||
|
||||
function restore(current: Owner) {
|
||||
for (const timer of current.pending)
|
||||
clock.clearTimeout(timer)
|
||||
current.pending.clear()
|
||||
for (const saved of current.saved) {
|
||||
if (target[saved.name] !== saved.wrapper)
|
||||
continue
|
||||
if (saved.descriptor)
|
||||
Object.defineProperty(target, saved.name, saved.descriptor)
|
||||
else
|
||||
delete target[saved.name]
|
||||
}
|
||||
if (target.feed === current.feed) {
|
||||
if (current.previousFeed)
|
||||
Object.defineProperty(target, 'feed', current.previousFeed)
|
||||
else
|
||||
delete target.feed
|
||||
}
|
||||
}
|
||||
|
||||
return (callback: (log: Log) => void) => {
|
||||
if (!owner) {
|
||||
const current: Owner = {
|
||||
listeners: new Set(),
|
||||
pending: new Set(),
|
||||
saved: [],
|
||||
previousFeed: Object.getOwnPropertyDescriptor(target, 'feed'),
|
||||
feed: { pointers: {}, src: { npm: 'https://npmjs.com/package/console-feed', github: 'https://github.com/samdenty99/console-feed' } },
|
||||
}
|
||||
owner = current
|
||||
try {
|
||||
for (const name of methods) {
|
||||
const original = target[name] as NativeMethod
|
||||
const wrapper: NativeMethod = function (...args) {
|
||||
Reflect.apply(original, this, args)
|
||||
const listeners = [...current.listeners]
|
||||
if (!listeners.length)
|
||||
return
|
||||
const timer = clock.setTimeout(() => {
|
||||
current.pending.delete(timer)
|
||||
const log = parse(name, name === 'error' ? args.map(errorArgument) : args)
|
||||
if (log) {
|
||||
let failed = false
|
||||
let failure: unknown
|
||||
for (const listener of listeners) {
|
||||
try {
|
||||
listener.callback?.(log)
|
||||
}
|
||||
catch (error) {
|
||||
if (!failed) {
|
||||
failed = true
|
||||
failure = error
|
||||
}
|
||||
else {
|
||||
clock.setTimeout(() => {
|
||||
throw error
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (failed)
|
||||
throw failure
|
||||
}
|
||||
}, 0)
|
||||
current.pending.add(timer)
|
||||
}
|
||||
current.feed.pointers[name] = original
|
||||
current.saved.push({ name, descriptor: Object.getOwnPropertyDescriptor(target, name), wrapper })
|
||||
target[name] = wrapper
|
||||
}
|
||||
target.feed = current.feed
|
||||
}
|
||||
catch (error) {
|
||||
restore(current)
|
||||
owner = undefined
|
||||
throw error
|
||||
}
|
||||
}
|
||||
const current = owner
|
||||
const listener: Listener = { callback }
|
||||
current.listeners.add(listener)
|
||||
return () => {
|
||||
listener.callback = undefined
|
||||
current.listeners.delete(listener)
|
||||
if (!current.listeners.size && owner === current) {
|
||||
owner = undefined
|
||||
restore(current)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type * as React from 'react'
|
||||
|
||||
export interface Log { method: string, id?: string, data?: unknown[] }
|
||||
export type Parser = (method: string, args: unknown[], id?: string) => Log | false
|
||||
export type NativeMethod = (this: unknown, ...args: unknown[]) => unknown
|
||||
export interface ConsoleTarget { [method: string]: unknown }
|
||||
export interface Clock {
|
||||
setTimeout: (callback: () => void, delay: number) => number
|
||||
clearTimeout: (id: number) => void
|
||||
}
|
||||
export interface Feed { Console: React.ComponentType<{ logs: Log[], variant: string }> }
|
||||
export interface Styled { div: (strings: TemplateStringsArray) => React.ComponentType<React.PropsWithChildren> }
|
||||
export type ReactRuntime = typeof React
|
||||
export interface DomRuntime {
|
||||
render: (element: React.ReactElement, container: Element) => unknown
|
||||
unmountComponentAtNode: (container: Element) => boolean
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { Feed, Log, Parser, ReactRuntime, Styled } from './types.ts'
|
||||
import { css } from './style.ts'
|
||||
import { createSubscriptions } from './subscriptions.ts'
|
||||
|
||||
export function createView(React: ReactRuntime, feed: Feed, styled: Styled, parse: Parser, methods: string[]) {
|
||||
const template = Object.freeze(Object.defineProperty([css], 'raw', { value: Object.freeze([css]) })) as unknown as TemplateStringsArray
|
||||
const Container = styled.div(template)
|
||||
const subscribe = createSubscriptions(window.console as unknown as Record<string, unknown>, methods, parse, window)
|
||||
interface State { logs: Log[], hover: boolean }
|
||||
return class ConsoleView extends React.Component<object, State> {
|
||||
scrollTimer: number | null = null
|
||||
consoleRef = React.createRef<HTMLDivElement>()
|
||||
private unsubscribe?: () => void
|
||||
private mounted = false
|
||||
|
||||
constructor(props: object) {
|
||||
super(props)
|
||||
this.onClear = this.onClear.bind(this)
|
||||
this.onMouseEnter = this.onMouseEnter.bind(this)
|
||||
this.onMouseLeave = this.onMouseLeave.bind(this)
|
||||
this.state = { logs: [], hover: false }
|
||||
}
|
||||
|
||||
override componentDidMount() {
|
||||
this.mounted = true
|
||||
this.unsubscribe = subscribe(log => this.add(log))
|
||||
}
|
||||
|
||||
add(log: Log) {
|
||||
if (!this.mounted)
|
||||
return
|
||||
this.setState(state => ({ logs: [...state.logs, log] }))
|
||||
const element = this.consoleRef.current
|
||||
if (!this.state.hover && element) {
|
||||
if (this.scrollTimer !== null)
|
||||
window.clearTimeout(this.scrollTimer)
|
||||
this.scrollTimer = window.setTimeout(() => {
|
||||
this.scrollTimer = null
|
||||
if (this.mounted && this.consoleRef.current === element)
|
||||
element.scrollTop = element.scrollHeight
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
|
||||
onClear() { this.setState({ logs: [] }) }
|
||||
onMouseEnter() { this.setState({ hover: true }) }
|
||||
onMouseLeave() { this.setState({ hover: false }) }
|
||||
|
||||
override componentWillUnmount() {
|
||||
this.mounted = false
|
||||
this.unsubscribe?.()
|
||||
this.unsubscribe = undefined
|
||||
if (this.scrollTimer !== null)
|
||||
window.clearTimeout(this.scrollTimer)
|
||||
this.scrollTimer = null
|
||||
}
|
||||
|
||||
override render() {
|
||||
const count = React.createElement('span', { className: 'console-header-number' }, this.state.logs.length)
|
||||
const title = React.createElement('div', { className: 'console-header-left' }, 'Console', ' ', count)
|
||||
const clear = React.createElement('div', { className: 'console-header-right', onClick: this.onClear }, 'Clear')
|
||||
const header = React.createElement('div', { className: 'console-header' }, title, clear)
|
||||
const logs = React.createElement(feed.Console, { logs: this.state.logs, variant: 'dark' })
|
||||
const content = React.createElement('div', { ref: this.consoleRef, className: 'console-component', onMouseEnter: this.onMouseEnter, onMouseLeave: this.onMouseLeave }, logs)
|
||||
return React.createElement(Container, null, header, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user