mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-13 15:56:15 -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,12 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import process from 'node:process'
|
||||
import { generateConsole } from './site-vendor/console/build.ts'
|
||||
|
||||
assert(process.argv.slice(2).every(arg => arg === '--check'), 'Use yarn build:console [--check]')
|
||||
const candidate = await generateConsole(process.cwd())
|
||||
const output = 'docs/assets/js/console.js'
|
||||
if (process.argv.includes('--check'))
|
||||
assert.equal(fs.readFileSync(output, 'utf8'), candidate, 'Generated console drift')
|
||||
else fs.writeFileSync(output, candidate)
|
||||
console.log('Console TS entry and view generated/checked; 100 frozen vendor modules and Parcel runtime retained.')
|
||||
@@ -55,6 +55,10 @@ members as Buffers. Compare `package/dist/codicon.ttf` directly with `fontPath`,
|
||||
and each non-null notice member with its frozen `source`. Do not pipe binary font
|
||||
output through PowerShell text redirection. No dependency installation is needed.
|
||||
|
||||
Follow-up: finish Monaco's broader bundled-component notice audit, then recover the
|
||||
console.js build and resolve remaining fonts/media. Do not upgrade these assets
|
||||
The desktop console's owned TS entry/view and lifecycle now build through
|
||||
`build:console` / `check:console`; see [console maintenance](console/README.md).
|
||||
Its other 100 Parcel modules remain frozen pending full provenance and notices.
|
||||
|
||||
Follow-up: finish Monaco's broader bundled-component notice audit, the console
|
||||
vendor provenance and remaining fonts/media. Do not upgrade these assets
|
||||
without verifying globals, AMD/worker paths, CSS, consoleLog and user interaction.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Desktop console maintenance
|
||||
|
||||
`yarn build:console` generates `docs/assets/js/console.js`; `yarn check:console`
|
||||
compares it without writing. Never edit that output. This is the desktop editor
|
||||
console, separate from the mobile vConsole integration.
|
||||
|
||||
## Modules and ownership
|
||||
|
||||
| File | Responsibility |
|
||||
| -------------------------- | ------------------------------------------------------------------------------------ |
|
||||
| `build.ts` | Verify frozen input, compile two owned modules, replace their Parcel function bodies |
|
||||
| `runtime/entry.ts` | Install React, ReactDOM and consoleLog globals; return the React render result |
|
||||
| `runtime/view.ts` | Component state, Clear, hover, scroll scheduling and unmount |
|
||||
| `runtime/subscriptions.ts` | One hook owner, subscriptions, queued delivery and descriptor restoration |
|
||||
| `runtime/errors.ts` | Preserve native error message and stack without mutating the input |
|
||||
| `runtime/style.ts` | Original stylesheet, decoded exactly from the frozen view |
|
||||
| `runtime/types.ts` | Structural interfaces for injected runtimes and parser |
|
||||
|
||||
The fixed upstream is `refactor/baselines/site-vendor/console-original.js`, checked
|
||||
by SHA-256. Only W5CS (view) and Focm (entry) bodies change. The other 100 modules,
|
||||
their dependency maps and Parcel runtime remain byte-for-byte intact. The dead
|
||||
`/index.js.map` trailer is removed; a complete upstream source map is unavailable.
|
||||
The entry injects existing React/ReactDOM 17.0.2, console-feed and
|
||||
styled-components 5.3.3. RxPG supplies the historical parser and V2JG the method
|
||||
list. Root `@types/react` 19.1.10 is used for type checking only; no React 19 runtime
|
||||
is included. These module IDs are a frozen build boundary, not a general Parcel
|
||||
patching API. Changing the vendor requires a separate provenance review.
|
||||
|
||||
## Data and cleanup
|
||||
|
||||
Each native call forwards once with its receiver, then queues parsing. Wrappers
|
||||
retain the old undefined return behavior. Count/timing/assert parsing runs once
|
||||
per call, and active viewers receive the same parsed record; raw object references
|
||||
remain available to the old inspector. Error-only logs add a missing error header
|
||||
before the historical stack conversion, preserving the original Error object.
|
||||
|
||||
The first subscriber captures method/feed descriptors and installs the hook.
|
||||
Additional viewers share it. Delivery snapshots subscribers when the call occurs;
|
||||
removed viewers do not receive it and later viewers do not receive old logs.
|
||||
The final subscriber cancels pending delivery and restores only wrappers and
|
||||
metadata still owned by this instance. Later external wrappers remain intact.
|
||||
An external wrapper retaining an old hook can forward native calls without
|
||||
retaining viewer callbacks. Failed installation rolls back. A throwing listener
|
||||
does not starve other viewers; its error remains observable asynchronously.
|
||||
No general interception of third-party console mutations is attempted.
|
||||
|
||||
The component owns its 200ms scroll timer and cancels it on unmount; the callback
|
||||
also verifies the live element. `consoleLog(element)` still returns the React
|
||||
component, and repeat mounting into the same container reuses it. The additive
|
||||
`consoleLog.unmount(element)` forwards ReactDOM's boolean result. The editor uses
|
||||
it on non-persisted pagehide. Persisted pagehide retains state; controlled lifecycle
|
||||
tests do not certify physical-device BFCache behavior.
|
||||
|
||||
```sh
|
||||
yarn build:console
|
||||
yarn check:console
|
||||
yarn typecheck:docs-tools
|
||||
yarn test:site-console
|
||||
yarn build:site-assets
|
||||
yarn test:browser test/browser/site-console.spec.js test/browser/site-editor.spec.js --workers=1
|
||||
```
|
||||
|
||||
Historical browser cases intentionally reproduce frozen defects; candidate cases
|
||||
prove their repairs independently. Unit tests check shared ownership, failed
|
||||
installation, callback errors, object identity, exact CSS and vendor preservation.
|
||||
No new dependency is required. Existing esbuild 0.27.7 and TypeScript 5.9.3 provide
|
||||
generation and structural parsing. Full console dependency provenance and notices
|
||||
remain SITE-07 / VENDOR-08; this build does not close them. See
|
||||
`refactor/console-modernization.md` for contracts and evidence.
|
||||
@@ -0,0 +1,48 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { createHash } from 'node:crypto'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { build } from 'esbuild'
|
||||
import ts from 'typescript'
|
||||
|
||||
export const upstreamSha256 = 'e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03'
|
||||
export const obsoleteMap = '//# sourceMappingURL=/index.js.map'
|
||||
|
||||
export function moduleRanges(code: string) {
|
||||
const source = ts.createSourceFile('console.js', code, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS)
|
||||
const ranges = new Map<string, { start: number, end: number }>()
|
||||
function visit(node: ts.Node) {
|
||||
if (ts.isPropertyAssignment(node) && ts.isStringLiteral(node.name) && ['W5CS', 'Focm'].includes(node.name.text)) {
|
||||
assert(ts.isArrayLiteralExpression(node.initializer))
|
||||
const fn = node.initializer.elements[0]
|
||||
assert(fn && ts.isFunctionExpression(fn))
|
||||
assert(!ranges.has(node.name.text), 'Duplicate owned console module')
|
||||
ranges.set(node.name.text, { start: fn.body.getStart(source) + 1, end: fn.body.end - 1 })
|
||||
}
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
visit(source)
|
||||
assert.equal(ranges.size, 2, 'Missing owned console modules')
|
||||
return ranges
|
||||
}
|
||||
|
||||
export async function generateConsole(root: string) {
|
||||
const upstream = fs.readFileSync(path.join(root, 'refactor/baselines/site-vendor/console-original.js'), 'utf8')
|
||||
assert.equal(createHash('sha256').update(upstream).digest('hex'), upstreamSha256, 'Frozen console source changed')
|
||||
const ranges = moduleRanges(upstream)
|
||||
const bodies = new Map<string, string>()
|
||||
for (const [id, entry] of [['W5CS', 'view'], ['Focm', 'entry']] as const) {
|
||||
const result = await build({ entryPoints: [path.join(root, `scripts/site-vendor/console/runtime/${entry}.ts`)], bundle: true, write: false, platform: 'browser', format: 'iife', globalName: '__consoleOwned', target: 'es2015', minify: true, legalComments: 'none', logLevel: 'silent' })
|
||||
const compiled = result.outputFiles[0]?.text
|
||||
assert(compiled)
|
||||
const expression = id === 'W5CS'
|
||||
? '__consoleOwned.createView(require("react"),require("console-feed"),require("styled-components").default,require("RxPG").default,require("V2JG").default)'
|
||||
: '__consoleOwned.install(require("react"),require("react-dom"),require("./Console").default,window)'
|
||||
bodies.set(id, `\n"use strict";${compiled}Object.defineProperty(exports,"__esModule",{value:true});exports.default=${expression};\n`)
|
||||
}
|
||||
let output = upstream
|
||||
for (const [id, range] of [...ranges].sort((a, b) => b[1].start - a[1].start))
|
||||
output = output.slice(0, range.start) + bodies.get(id) + output.slice(range.end)
|
||||
assert(output.endsWith(obsoleteMap), 'Unexpected original console source map trailer')
|
||||
return output.slice(0, -obsoleteMap.length)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@
|
||||
"../packages/artplayer-vitepress/build/**/*.ts",
|
||||
"build-vconsole.mjs",
|
||||
"browser-validation/**/*.ts",
|
||||
"../test/browser/chapter-hover.ts"
|
||||
"../test/browser/chapter-hover.ts",
|
||||
"build-console.mjs"
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user