diff --git a/docs/assets/js/common.js b/docs/assets/js/common.js index 94c52ff28..27c57bc87 100644 --- a/docs/assets/js/common.js +++ b/docs/assets/js/common.js @@ -297,6 +297,9 @@ host.consoleLog(output); const controller = new AbortController(); const cleanups = []; + cleanups.push(() => { + host.consoleLog.unmount?.(output); + }); const listen = (target, type, callback) => { target.addEventListener(type, callback); cleanups.push(() => target.removeEventListener(type, callback)); diff --git a/docs/assets/js/console.js b/docs/assets/js/console.js index 137d36c21..683a2e429 100644 --- a/docs/assets/js/console.js +++ b/docs/assets/js/console.js @@ -202,8 +202,45 @@ var t,e,n=module.exports={};function r(){throw new Error("setTimeout has not bee var process = require("process"); var e=require("process");Object.defineProperty(exports,"__esModule",{value:!0}),exports.StyleSheetContext=exports.StyleSheetConsumer=exports.ServerStyleSheet=void 0,exports.StyleSheetManager=fe,exports.ThemeContext=exports.ThemeConsumer=void 0,exports.ThemeProvider=Fe,exports.__PRIVATE__=void 0,exports.createGlobalStyle=We,exports.css=we,exports.default=void 0,exports.isStyledComponent=S,exports.keyframes=$e,exports.withTheme=exports.version=exports.useTheme=void 0;var t=require("react-is"),r=c(require("react")),n=u(require("shallowequal")),o=u(require("@emotion/stylis")),i=u(require("@emotion/unitless")),s=u(require("@emotion/is-prop-valid")),a=u(require("hoist-non-react-statics"));function u(e){return e&&e.__esModule?e:{default:e}}function l(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,r=new WeakMap;return(l=function(e){return e?r:t})(e)}function c(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var r=l(t);if(r&&r.has(e))return r.get(e);var n={},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var i in e)if("default"!==i&&Object.prototype.hasOwnProperty.call(e,i)){var s=o?Object.getOwnPropertyDescriptor(e,i):null;s&&(s.get||s.set)?Object.defineProperty(n,i,s):n[i]=e[i]}return n.default=e,r&&r.set(e,n),n}function f(e){return(f="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function h(){return(h=Object.assign||function(e){for(var t=1;t1?t-1:0),n=1;n0?" Args: "+r.join(", "):""))}exports.version=w;var R=function(){function e(e){this.groupSizes=new Uint32Array(512),this.length=512,this.tag=e}var t=e.prototype;return t.indexOfGroup=function(e){for(var t=0,r=0;r=this.groupSizes.length){for(var r=this.groupSizes,n=r.length,o=n;e>=o;)(o<<=1)<0&&j(16,""+e);this.groupSizes=new Uint32Array(o),this.groupSizes.set(r),this.length=o;for(var i=n;i=this.length||0===this.groupSizes[e])return t;for(var r=this.groupSizes[e],n=this.indexOfGroup(e),o=n+r,i=n;i=N&&(N=t+1),_.set(e,t),O.set(t,e)},M="style["+b+'][data-styled-version="5.3.3"]',z=new RegExp("^"+b+'\\.g(\\d+)\\[id="([\\w\\d-]+)"\\].*?"([^"]*)'),F=function(e,t,r){for(var n,o=r.split(","),i=0,s=o.length;i=0;r--){var n=t[r];if(n&&1===n.nodeType&&n.hasAttribute(b))return n}}(r),i=void 0!==o?o.nextSibling:null;n.setAttribute(b,"active"),n.setAttribute("data-styled-version","5.3.3");var s=G();return s&&n.setAttribute("nonce",s),r.insertBefore(n,i),n},D=function(){function e(e){var t=this.element=B(e);t.appendChild(document.createTextNode("")),this.sheet=function(e){if(e.sheet)return e.sheet;for(var t=document.styleSheets,r=0,n=t.length;r=0){var r=document.createTextNode(t),n=this.nodes[e];return this.element.insertBefore(r,n||null),this.length++,!0}return!1},t.deleteRule=function(e){this.element.removeChild(this.nodes[e]),this.length--},t.getRule=function(e){return e0&&(l+=e+",")}),n+=""+a+u+'{content:"'+l+'"}/*!sc*/\n'}}}return n}(this)},e}(),U=/(a)(d)/gi,J=function(e){return String.fromCharCode(e+(e>25?39:97))};function Y(e){var t,r="";for(t=Math.abs(e);t>52;t=t/52|0)r=J(t%52)+r;return(J(t%52)+r).replace(U,"$1-$2")}var Z=function(e,t){for(var r=t.length;r;)e=33*e^t.charCodeAt(--r);return e},K=function(e){return Z(5381,e)};function Q(e){for(var t=0;t>>0);if(!t.hasNameForId(n,s)){var a=r(i,"."+s,void 0,n);t.insertRules(n,s,a)}o.push(s),this.staticRulesId=s}else{for(var u=this.rules.length,l=Z(this.baseHash,r.hash),c="",f=0;f>>0);if(!t.hasNameForId(n,v)){var g=r(c,"."+v,void 0,n);t.insertRules(n,v,g)}o.push(v)}}return o.join(" ")},e}(),te=/^\s*\/\/.*$/gm,re=[":","[",".","#"];function ne(e){var t,r,n,i,s=void 0===e?g:e,a=s.options,u=void 0===a?g:a,l=s.plugins,c=void 0===l?v:l,f=new o.default(u),h=[],d=function(e){function t(t){if(t)try{e(t+"}")}catch(e){}}return function(r,n,o,i,s,a,u,l,c,f){switch(r){case 1:if(0===c&&64===n.charCodeAt(0))return e(n+";"),"";break;case 2:if(0===l)return n+"/*|*/";break;case 3:switch(l){case 102:case 112:return e(o[0]+n),"";default:return n+(0===f?"/*|*/":"")}case-2:n.split("/*|*/}").forEach(t)}}}(function(e){h.push(e)}),p=function(e,n,o){return 0===n&&-1!==re.indexOf(o[r.length])||o.match(i)?e:"."+t};function m(e,o,s,a){void 0===a&&(a="&");var u=e.replace(te,""),l=o&&s?s+" "+o+" { "+u+" }":u;return t=a,r=o,n=new RegExp("\\"+r+"\\b","g"),i=new RegExp("(\\"+r+"\\b){2,}"),f(s||!o?"":o,l)}return f.use([].concat(c,[function(e,t,o){2===e&&o.length&&o[0].lastIndexOf(r)>0&&(o[0]=o[0].replace(n,p))},d,function(e){if(-2===e){var t=h;return h=[],t}}])),m.hash=c.length?c.reduce(function(e,t){return t.name||j(15),Z(e,t.name)},5381).toString():"",m}var oe=r.default.createContext(),ie=oe.Consumer,se=r.default.createContext(),ae=(se.Consumer,new V),ue=ne();function le(){return(0,r.useContext)(oe)||ae}function ce(){return(0,r.useContext)(se)||ue}function fe(e){var t=(0,r.useState)(e.stylisPlugins),o=t[0],i=t[1],s=le(),a=(0,r.useMemo)(function(){var t=s;return e.sheet?t=e.sheet:e.target&&(t=t.reconstructWithOptions({target:e.target},!1)),e.disableCSSOMInjection&&(t=t.reconstructWithOptions({useCSSOMInjection:!1})),t},[e.disableCSSOMInjection,e.sheet,e.target]),u=(0,r.useMemo)(function(){return ne({options:{prefix:!e.disableVendorPrefixes},plugins:o})},[e.disableVendorPrefixes,o]);return(0,r.useEffect)(function(){(0,n.default)(o,e.stylisPlugins)||i(e.stylisPlugins)},[e.stylisPlugins]),r.default.createElement(oe.Provider,{value:a},r.default.createElement(se.Provider,{value:u},e.children))}exports.StyleSheetConsumer=ie,exports.StyleSheetContext=oe;var he=function(){function e(e,t){var r=this;this.inject=function(e,t){void 0===t&&(t=ue);var n=r.name+t.hash;e.hasNameForId(r.id,n)||e.insertRules(r.id,n,t(r.rules,n,"@keyframes"))},this.toString=function(){return j(12,String(r.name))},this.name=e,this.id="sc-keyframes-"+e,this.rules=t}return e.prototype.getName=function(e){return void 0===e&&(e=ue),this.name+e.hash},e}(),de=/([A-Z])/,pe=/([A-Z])/g,ve=/^ms-/,ge=function(e){return"-"+e.toLowerCase()};function me(e){return de.test(e)?e.replace(pe,ge).replace(ve,"-ms-"):e}var ye=function(e){return null==e||!1===e||""===e};function Se(e,t,r,n){if(Array.isArray(e)){for(var o,s=[],a=0,u=e.length;a1?t-1:0),n=1;n?@[\\\]^`{|}~-]+/g,je=/(^-|-$)/g;function Re(e){return e.replace(Pe,"-").replace(je,"")}var _e=function(e){return Y(K(e)>>>0)};function Oe(e){return"string"==typeof e&&!0}var Ne=function(e){return"function"==typeof e||"object"==f(e)&&null!==e&&!Array.isArray(e)},Ee=function(e){return"__proto__"!==e&&"constructor"!==e&&"prototype"!==e};function Te(e,t,r){var n=e[r];Ne(t)&&Ne(n)?ke(n,t):e[r]=t}function ke(e){for(var t=arguments.length,r=new Array(t>1?t-1:0),n=1;n=0||(o[r]=e[r]);return o}(t,["componentId"]),i=r&&r+"-"+(Oe(e)?e:Re(y(e)));return Ge(e,h({},o,{attrs:w,componentId:i}),n)},Object.defineProperty(C,"defaultProps",{get:function(){return this._foldedDefaultProps},set:function(t){this._foldedDefaultProps=o?ke({},e.defaultProps,t):t}}),C.toString=function(){return"."+C.styledComponentId},i&&(0,a.default)(C,e,{attrs:!0,componentStyle:!0,displayName:!0,foldedComponentIds:!0,shouldForwardProp:!0,styledComponentId:!0,target:!0,withComponent:!0}),C}var Be=function(e){return function e(r,n,o){if(void 0===o&&(o=g),!(0,t.isValidElementType)(n))return j(1,String(n));var i=function(){return r(n,o,we.apply(void 0,arguments))};return i.withConfig=function(t){return e(r,n,h({},o,{},t))},i.attrs=function(t){return e(r,n,h({},o,{attrs:Array.prototype.concat(o.attrs,t).filter(Boolean)}))},i}(Ge,e)};["a","abbr","address","area","article","aside","audio","b","base","bdi","bdo","big","blockquote","body","br","button","canvas","caption","cite","code","col","colgroup","data","datalist","dd","del","details","dfn","dialog","div","dl","dt","em","embed","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","iframe","img","input","ins","kbd","keygen","label","legend","li","link","main","map","mark","marquee","menu","menuitem","meta","meter","nav","noscript","object","ol","optgroup","option","output","p","param","picture","pre","progress","q","rp","rt","ruby","s","samp","script","section","select","small","source","span","strong","style","sub","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","title","tr","track","u","ul","var","video","wbr","circle","clipPath","defs","ellipse","foreignObject","g","image","line","linearGradient","marker","mask","path","pattern","polygon","polyline","radialGradient","rect","stop","svg","text","textPath","tspan"].forEach(function(e){Be[e]=Be(e)});var De=function(){function e(e,t){this.rules=e,this.componentId=t,this.isStatic=Q(e),V.registerId(this.componentId+1)}var t=e.prototype;return t.createStyles=function(e,t,r,n){var o=n(Se(this.rules,t,r,n).join(""),""),i=this.componentId+e;r.insertRules(i,i,o)},t.removeStyles=function(e,t){t.clearRules(this.componentId+e)},t.renderStyles=function(e,t,r,n){e>2&&V.registerId(this.componentId+e),this.removeStyles(e,r),this.createStyles(e,t,r,n)},e}();function We(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),o=1;o1?t-1:0),n=1;n"+t+""},this.getStyleTags=function(){return e.sealed?j(2):e._emitSheetCSS()},this.getStyleElement=function(){var t;if(e.sealed)return j(2);var n=((t={})[b]="",t["data-styled-version"]="5.3.3",t.dangerouslySetInnerHTML={__html:e.instance.toString()},t),o=G();return o&&(n.nonce=o),[r.default.createElement("style",h({},n,{key:"sc-0-0"}))]},this.seal=function(){e.sealed=!0},this.instance=new V({isServer:!0}),this.sealed=!1}var t=e.prototype;return t.collectStyles=function(e){return this.sealed?j(2):r.default.createElement(fe,{sheet:this.instance},e)},t.interleaveWithNodeStream=function(e){return j(3)},e}(),He=function(e){var t=r.default.forwardRef(function(t,n){var o=(0,r.useContext)(Me),i=e.defaultProps,s=Ae(t,o,i);return r.default.createElement(e,h({},t,{theme:s,ref:n}))});return(0,a.default)(t,e),t.displayName="WithTheme("+y(e)+")",t},Ve=function(){return(0,r.useContext)(Me)},Ue={StyleSheet:V,masterSheet:ae};exports.__PRIVATE__=Ue,exports.useTheme=Ve,exports.withTheme=He,exports.ServerStyleSheet=Le;var Je=Be;exports.default=Je; },{"react-is":"H1RQ","react":"n8MK","shallowequal":"NKxe","@emotion/stylis":"uxSu","@emotion/unitless":"qy56","@emotion/is-prop-valid":"m5O0","hoist-non-react-statics":"LCQI","process":"KIG8"}],"W5CS":[function(require,module,exports) { -"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=void 0;var e,t=r(require("react")),n=require("console-feed"),o=r(require("styled-components"));function r(e){return e&&e.__esModule?e:{default:e}}function l(e){return(l="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function i(e){return s(e)||u(e)||a(e)||c()}function c(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function a(e,t){if(e){if("string"==typeof e)return f(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?f(e,t):void 0}}function u(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}function s(e){if(Array.isArray(e))return f(e)}function f(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,o=new Array(t);n{var v=Object.defineProperty;var C=Object.getOwnPropertyDescriptor;var E=Object.getOwnPropertyNames;var L=Object.prototype.hasOwnProperty;var O=(e,o)=>{for(var s in o)v(e,s,{get:o[s],enumerable:!0})},j=(e,o,s,i)=>{if(o&&typeof o=="object"||typeof o=="function")for(let n of E(o))!L.call(e,n)&&n!==s&&v(e,n,{get:()=>o[n],enumerable:!(i=C(o,n))||i.enumerable});return e};var P=e=>j(v({},"__esModule",{value:!0}),e);var D={};O(D,{createView:()=>N});var w=` + 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; + } +`;function k(e){try{if(Object.prototype.toString.call(e)!=="[object Error]")return e;let o=e,s=o.stack,i=o.message;if(typeof s!="string"||!i)return e;let n=o.name?`${o.name}: ${i}`:i;return s.startsWith(n)?e:`${n} +${s}`}catch(o){return e}}function x(e,o,s,i){let n;function p(l){for(let r of l.pending)i.clearTimeout(r);l.pending.clear();for(let r of l.saved)e[r.name]===r.wrapper&&(r.descriptor?Object.defineProperty(e,r.name,r.descriptor):delete e[r.name]);e.feed===l.feed&&(l.previousFeed?Object.defineProperty(e,"feed",l.previousFeed):delete e.feed)}return l=>{if(!n){let c={listeners:new Set,pending:new Set,saved:[],previousFeed:Object.getOwnPropertyDescriptor(e,"feed"),feed:{pointers:{},src:{npm:"https://npmjs.com/package/console-feed",github:"https://github.com/samdenty99/console-feed"}}};n=c;try{for(let t of o){let a=e[t],d=function(...u){Reflect.apply(a,this,u);let h=[...c.listeners];if(!h.length)return;let m=i.setTimeout(()=>{var T;c.pending.delete(m);let y=s(t,t==="error"?u.map(k):u);if(y){let g=!1,M;for(let b of h)try{(T=b.callback)==null||T.call(b,y)}catch(S){g?i.setTimeout(()=>{throw S},0):(g=!0,M=S)}if(g)throw M}},0);c.pending.add(m)};c.feed.pointers[t]=a,c.saved.push({name:t,descriptor:Object.getOwnPropertyDescriptor(e,t),wrapper:d}),e[t]=d}e.feed=c.feed}catch(t){throw p(c),n=void 0,t}}let r=n,f={callback:l};return r.listeners.add(f),()=>{f.callback=void 0,r.listeners.delete(f),!r.listeners.size&&n===r&&(n=void 0,p(r))}}}function N(e,o,s,i,n){let p=Object.freeze(Object.defineProperty([w],"raw",{value:Object.freeze([w])})),l=s.div(p),r=x(window.console,n,i,window);return class extends e.Component{constructor(t){super(t);this.scrollTimer=null;this.consoleRef=e.createRef();this.mounted=!1;this.onClear=this.onClear.bind(this),this.onMouseEnter=this.onMouseEnter.bind(this),this.onMouseLeave=this.onMouseLeave.bind(this),this.state={logs:[],hover:!1}}componentDidMount(){this.mounted=!0,this.unsubscribe=r(t=>this.add(t))}add(t){if(!this.mounted)return;this.setState(d=>({logs:[...d.logs,t]}));let a=this.consoleRef.current;!this.state.hover&&a&&(this.scrollTimer!==null&&window.clearTimeout(this.scrollTimer),this.scrollTimer=window.setTimeout(()=>{this.scrollTimer=null,this.mounted&&this.consoleRef.current===a&&(a.scrollTop=a.scrollHeight)},200))}onClear(){this.setState({logs:[]})}onMouseEnter(){this.setState({hover:!0})}onMouseLeave(){this.setState({hover:!1})}componentWillUnmount(){var t;this.mounted=!1,(t=this.unsubscribe)==null||t.call(this),this.unsubscribe=void 0,this.scrollTimer!==null&&window.clearTimeout(this.scrollTimer),this.scrollTimer=null}render(){let t=e.createElement("span",{className:"console-header-number"},this.state.logs.length),a=e.createElement("div",{className:"console-header-left"},"Console"," ",t),d=e.createElement("div",{className:"console-header-right",onClick:this.onClear},"Clear"),u=e.createElement("div",{className:"console-header"},a,d),h=e.createElement(o.Console,{logs:this.state.logs,variant:"dark"}),m=e.createElement("div",{ref:this.consoleRef,className:"console-component",onMouseEnter:this.onMouseEnter,onMouseLeave:this.onMouseLeave},h);return e.createElement(l,null,u,m)}}}return P(D);})(); +Object.defineProperty(exports,"__esModule",{value:true});exports.default=__consoleOwned.createView(require("react"),require("console-feed"),require("styled-components").default,require("RxPG").default,require("V2JG").default); },{"react":"n8MK","console-feed":"m6b6","styled-components":"tFSs"}],"Focm":[function(require,module,exports) { -"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=void 0;var e=u(require("react")),t=u(require("react-dom")),r=u(require("./Console"));function u(e){return e&&e.__esModule?e:{default:e}}var o=function(u){return t.default.render(e.default.createElement(r.default,null),u)};window.React=e.default,window.ReactDOM=t.default,window.consoleLog=o;var a=o;exports.default=a; +"use strict";"use strict";var __consoleOwned=(()=>{var r=Object.defineProperty;var i=Object.getOwnPropertyDescriptor;var c=Object.getOwnPropertyNames;var R=Object.prototype.hasOwnProperty;var l=(e,n)=>{for(var o in n)r(e,o,{get:n[o],enumerable:!0})},p=(e,n,o,m)=>{if(n&&typeof n=="object"||typeof n=="function")for(let t of c(n))!R.call(e,t)&&t!==o&&r(e,t,{get:()=>n[t],enumerable:!(m=i(n,t))||m.enumerable});return e};var s=e=>p(r({},"__esModule",{value:!0}),e);var d={};l(d,{install:()=>a});function a(e,n,o,m){let t=u=>n.render(e.createElement(o),u);return t.unmount=u=>n.unmountComponentAtNode(u),m.React=e,m.ReactDOM=n,m.consoleLog=t,t}return s(d);})(); +Object.defineProperty(exports,"__esModule",{value:true});exports.default=__consoleOwned.install(require("react"),require("react-dom"),require("./Console").default,window); },{"react":"n8MK","react-dom":"NKHc","./Console":"W5CS"}]},{},["Focm"], null) -//# sourceMappingURL=/index.js.map \ No newline at end of file diff --git a/package.json b/package.json index a1fc6c76a..595cf097a 100644 --- a/package.json +++ b/package.json @@ -45,10 +45,10 @@ "check:toolchain": "node scripts/check-toolchain.mjs", "lint:fix": "eslint \"packages/*/{src,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"packages/artplayer-vitepress/{browser,build}/**/*.ts\" \"scripts/{docs-smoke,editor-declarations,documentation,site-build,library,pages,site-vendor,browser-validation}/**/*.ts\" \"scripts/plugin/*.{js,ts}\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" \"playwright*.config.js\" --fix", "check:plan": "node refactor/scripts/plan.mjs --check", - "test:node": "yarn test:unit && node --test test/toolchain.test.js test/build-docs.test.js test/package-check.test.js test/declarations.test.js test/editor-types.test.js test/coverage.test.js test/performance-report.test.js test/media-gate.test.js test/ci-summary.test.js test/package-runtime.test.js test/library-build.test.js test/site-loading.test.js test/documentation-pipeline.test.js test/site-build.test.js test/site-markdown.test.js test/site-editor.test.js test/plugin-scaffold.test.js test/dev-server.test.js test/pages-artifact.test.js test/site-notices.test.js test/vconsole-lifecycle.test.js test/browser-validation.test.js test/rollback-files.test.js test/pages-recovery.test.js", + "test:node": "yarn test:unit && node --test test/toolchain.test.js test/build-docs.test.js test/package-check.test.js test/declarations.test.js test/editor-types.test.js test/coverage.test.js test/performance-report.test.js test/media-gate.test.js test/ci-summary.test.js test/package-runtime.test.js test/library-build.test.js test/site-loading.test.js test/documentation-pipeline.test.js test/site-build.test.js test/site-markdown.test.js test/site-editor.test.js test/plugin-scaffold.test.js test/dev-server.test.js test/pages-artifact.test.js test/site-notices.test.js test/site-console.test.js test/vconsole-lifecycle.test.js test/browser-validation.test.js test/rollback-files.test.js test/pages-recovery.test.js", "test:baseline": "node --test refactor/scripts/*.test.mjs", - "ci:check": "yarn check:toolchain --strict && yarn check:commits --report && yarn check:impact --report && yarn check:ci && yarn test:contracts && yarn check:contracts --report && yarn check:plan && yarn check:release-ledger && yarn check:rollback-inventory && yarn lint && yarn typecheck:library && yarn typecheck:scaffold && yarn typecheck:docs-tools && yarn check:docs-smoke && yarn check:editor-types && yarn check:llm && yarn check:vconsole && yarn check:site-notices && yarn typecheck:site-assets && yarn check:site-assets && yarn check:types && yarn typecheck && yarn typecheck:react && yarn lint:react && yarn typecheck:vue && yarn lint:vue && yarn test", - "ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:llm && yarn build:vconsole && yarn build:site-notices && yarn build:test && yarn build:docs && yarn test:imports", + "ci:check": "yarn check:toolchain --strict && yarn check:commits --report && yarn check:impact --report && yarn check:ci && yarn test:contracts && yarn check:contracts --report && yarn check:plan && yarn check:release-ledger && yarn check:rollback-inventory && yarn lint && yarn typecheck:library && yarn typecheck:scaffold && yarn typecheck:docs-tools && yarn check:docs-smoke && yarn check:editor-types && yarn check:llm && yarn check:vconsole && yarn check:console && yarn check:site-notices && yarn typecheck:site-assets && yarn check:site-assets && yarn check:types && yarn typecheck && yarn typecheck:react && yarn lint:react && yarn typecheck:vue && yarn lint:vue && yarn test", + "ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:llm && yarn build:vconsole && yarn build:console && yarn build:site-notices && yarn build:test && yarn build:docs && yarn test:imports", "test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js test/asr-distribution.test.js", "typecheck": "node scripts/typecheck.mjs", "test:unit": "node --test test/danmuku-setting.test.js test/danmuku-heatmap.test.js test/danmuku-renderer.test.js test/danmuku-scheduler.test.js test/danmuku-worker-client.test.js test/danmuku-input.test.js test/danmuku-parser.test.js test/danmuku-failures.test.js test/danmuku-mask-failures.test.js test/danmuku-mask-lifecycle.test.js test/asr-encoding.test.js test/asr-lifecycle.test.js test/asr-audio.test.js test/asr.test.js test/jassub.test.js test/jassub-registration.test.js test/jassub-runtime.test.js test/jassub-vendor-lifecycle.test.js test/jassub-frame-polyfill.test.js test/jassub-offscreen.test.js test/multiple-subtitles-ass.test.js test/multiple-subtitles-caption.test.js test/multiple-subtitles-merge.test.js test/multiple-subtitles-entities.test.js test/multiple-subtitles-lifecycle.test.js test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js test/vtt-thumbnail-parser.test.js test/vtt-thumbnail-lifecycle.test.js test/vtt-thumbnail.test.js test/auto-thumbnail-exports.test.js test/auto-thumbnail-canvas.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-encoding.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js test/thumbnail-emitter.test.js test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe-lifecycle.test.js test/iframe.test.js test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-entry.test.js test/mediabunny-capability.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast-exports.test.js test/vast.test.js test/vast-lifecycle.test.js test/vast-compatibility.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js test/asr-fallback-routing.test.js test/asr-local-example.test.js test/asr-explicit-capture.test.js test/chromecast.test.js test/chromecast-failures.test.js test/chromecast-runtime.test.js", @@ -161,7 +161,10 @@ "test:rollback:pages": "node scripts/pages-recovery.mjs", "test:rollback:pages:browser": "node test/pages-recovery-browser.mjs", "test:bun-install": "node refactor/scripts/bun-evaluation.mjs", - "test:chapter-native": "playwright test --config playwright.chapter-native.config.js" + "test:chapter-native": "playwright test --config playwright.chapter-native.config.js", + "build:console": "node scripts/build-console.mjs", + "check:console": "node scripts/build-console.mjs --check", + "test:site-console": "node --test test/site-console.test.js" }, "browserslist": "last 1 Chrome version", "devDependencies": { diff --git a/packages/artplayer-vitepress/README.md b/packages/artplayer-vitepress/README.md index 260c3dd88..34d69b2e5 100644 --- a/packages/artplayer-vitepress/README.md +++ b/packages/artplayer-vitepress/README.md @@ -61,6 +61,9 @@ The repository generators have different ownership: - `scripts/build-i18n.js`: typed standalone language builds, staged before replacing distribution and `docs/compiled/i18n/` together; legacy globals and paths remain. - `scripts/build-docs.js`: this VitePress build through pinned Yarn and staged output. +- `scripts/build-console.mjs`: owned desktop console TS entry/view and shared log + lifecycle, keeping the original React/Parcel runtime. Run `build:console` after + edits and `check:console` for drift. See [console maintenance](../../scripts/site-vendor/console/README.md). - `scripts/build-llm.js`: offline source-preserving `docs/llms.txt` and fingerprint manifest. `yarn check:llm` checks drift in CI; `ci:build` regenerates after types. - `scripts/trans-docs.js`: local plan by default; explicit `--remote` creates a @@ -94,8 +97,8 @@ current rules and verified scope. Full site/search acceptance remains SITE-05. SITE-07 now checks frozen Monaco/vConsole files and generates verbatim upstream texts under `docs/licenses/` with `build:site-notices` / `check:site-notices`. -See `scripts/site-vendor/README.md`: the vConsole upstream LICENSE is incomplete, -and its WebKit destruction regression remains open. The notices index explicitly +See `scripts/site-vendor/README.md`: vConsole sources/notices and its destruction +fix now have separate evidence. The notices index explicitly does not clear the console bundle, fonts, media or the rest of the site. `scripts/projects.js` excludes this workspace from the 21 library builds. diff --git a/packages/artplayer-vitepress/browser/README.md b/packages/artplayer-vitepress/browser/README.md index 5d83d9fa1..7a1b48256 100644 --- a/packages/artplayer-vitepress/browser/README.md +++ b/packages/artplayer-vitepress/browser/README.md @@ -70,7 +70,9 @@ Imports preserve sequential JS/CSS insertion and the last lowercased `[name]` la Unsupported extensions are ignored; local labels are not persistable dependency URLs. File errors now reject, cancellation removes handlers, and batches are serialized. On non-persisted pagehide the editor cleans listeners, models, declaration libraries, -FileReaders, imported nodes and players. Executed script globals cannot be undone by +FileReaders, imported nodes, players and the desktop console viewer. The optional +`consoleLog.unmount` adapter releases its hook and pending work; older console +bundles without the adapter remain loadable. Executed script globals cannot be undone by removing script nodes. Persisted pagehide keeps state; synthetic lifecycle tests do not certify physical-device BFCache behavior. diff --git a/packages/artplayer-vitepress/browser/editor-host.ts b/packages/artplayer-vitepress/browser/editor-host.ts index 0b0a6bafa..69b4c498c 100644 --- a/packages/artplayer-vitepress/browser/editor-host.ts +++ b/packages/artplayer-vitepress/browser/editor-host.ts @@ -25,7 +25,7 @@ export type EditorHost = Window & { } monaco: typeof Monaco require: AmdLoader - consoleLog: (element: HTMLElement) => void + consoleLog: ((element: HTMLElement) => unknown) & { unmount?: (element: HTMLElement) => boolean } ArtplayerDocsLoader: ReturnType & { exampleSource: typeof exampleSource parameters: typeof parameters diff --git a/packages/artplayer-vitepress/browser/editor.ts b/packages/artplayer-vitepress/browser/editor.ts index 01ffe837e..c2e292c3e 100644 --- a/packages/artplayer-vitepress/browser/editor.ts +++ b/packages/artplayer-vitepress/browser/editor.ts @@ -25,6 +25,9 @@ export async function installEditor(host: EditorHost): Promise { host.consoleLog(output) const controller = new AbortController() const cleanups: (() => void)[] = [] + cleanups.push(() => { + host.consoleLog.unmount?.(output) + }) const listen = ( target: EventTarget, type: string, diff --git a/refactor/baselines/site-console01-validation.json b/refactor/baselines/site-console01-validation.json new file mode 100644 index 000000000..4037847c8 --- /dev/null +++ b/refactor/baselines/site-console01-validation.json @@ -0,0 +1,1005 @@ +{ + "schemaVersion": 1, + "task": "SITE-CONSOLE-01", + "recordedAt": "2026-09-14T22:13:21.852Z", + "environment": { + "node": "v24.21.0", + "yarn": "1.22.22", + "platform": "win32" + }, + "inputs": [ + { + "path": "docs/assets/js/console.js", + "sha256": "5644af7bb35bb6d0bcfa7eeae9a21b406ac4fc1b499a6c797a6534c7d33f0678", + "bytes": 340137 + }, + { + "path": "docs/assets/js/common.js", + "sha256": "63877e6800bcd9de557ac3f785500aa173bca856e6fc99ee1005810e59806c93", + "bytes": 14201 + }, + { + "path": "scripts/build-console.mjs", + "sha256": "375551c470fd746a7d6ed499c17425d8ca21c9253e90b6ba4bcc5146761b823f", + "bytes": 642 + }, + { + "path": "scripts/site-vendor/console/runtime/entry.ts", + "sha256": "976daec63b94e7899cd37d65cf8a9e692e1e3bd3e7bcd5a7eb49caeb2a6a0ab3", + "bytes": 475 + }, + { + "path": "scripts/site-vendor/console/runtime/errors.ts", + "sha256": "73b65c381a86a274fc0ce7939beb5d69c57f4140bc600e87b0bea44672db69ae", + "bytes": 567 + }, + { + "path": "scripts/site-vendor/console/runtime/style.ts", + "sha256": "2c67239a00bba96df16676de9a8323c416cafbba9437efa1110f72e062be199f", + "bytes": 778 + }, + { + "path": "scripts/site-vendor/console/runtime/subscriptions.ts", + "sha256": "42a5e6c4d47f7c0ea10bf00966741ca8d4dc8736f5ea2c2ba680c4a42345c437", + "bytes": 3558 + }, + { + "path": "scripts/site-vendor/console/runtime/types.ts", + "sha256": "4ba39c9c79f2e34746b26d55b0ccb47c8d2a6119ae6d8925960710040c404f7f", + "bytes": 855 + }, + { + "path": "scripts/site-vendor/console/runtime/view.ts", + "sha256": "e0968d7e63648775ca8693167706e25965de43f03771f6a71d97b620e72b85d4", + "bytes": 2983 + }, + { + "path": "scripts/site-vendor/console/build.ts", + "sha256": "512e3bb8cf89c471c912394311b473e243c2d4a60af6639700ee85f1c80f1d47", + "bytes": 2777 + }, + { + "path": "packages/artplayer-vitepress/browser/editor.ts", + "sha256": "132f6e12333aac3b54e4eea9f54469a26a9e676ea688034168c3a1ef134ba668", + "bytes": 5310 + }, + { + "path": "packages/artplayer-vitepress/browser/editor-host.ts", + "sha256": "4c8f657850f670528f513789dfcf798db10b6e6b0f0335ed4fbd822a09799b8a", + "bytes": 935 + } + ], + "historical": { + "path": "refactor/baselines/site-vendor/console-original.js", + "sha256": "e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03", + "bytes": 340306 + }, + "red": { + "path": "refactor/.cache/console01-red-report/report.json", + "stats": { + "startTime": "2026-09-14T21:56:59.846Z", + "duration": 56037.924, + "expected": 1, + "skipped": 0, + "unexpected": 11, + "flaky": 0 + } + }, + "initialGreen": { + "path": "refactor/.cache/console01-first-green-report/report.json", + "stats": { + "startTime": "2026-09-14T21:59:39.728Z", + "duration": 63564.971000000005, + "expected": 57, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + } + }, + "finalBrowser": { + "path": "refactor/.cache/console01-final-browser-report/report.json", + "stats": { + "startTime": "2026-09-14T22:08:12.595Z", + "duration": 72023.212, + "expected": 63, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "cases": [ + { + "title": "current console globals, returned component and repeated mount retain their interfaces", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "input": { + "historical": false, + "path": "docs/assets/js/console.js", + "sha256": "5644af7bb35bb6d0bcfa7eeae9a21b406ac4fc1b499a6c797a6534c7d33f0678" + }, + "pageErrors": [] + }, + { + "title": "current console renders levels and raw objects; Clear resets the visible log count", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "input": { + "historical": false, + "path": "docs/assets/js/console.js", + "sha256": "5644af7bb35bb6d0bcfa7eeae9a21b406ac4fc1b499a6c797a6534c7d33f0678" + }, + "pageErrors": [] + }, + { + "title": "current console scrolls new logs and preserves the pointer hover pause", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "input": { + "historical": false, + "path": "docs/assets/js/console.js", + "sha256": "5644af7bb35bb6d0bcfa7eeae9a21b406ac4fc1b499a6c797a6534c7d33f0678" + }, + "pageErrors": [] + }, + { + "title": "historical console globals, returned component and repeated mount retain their interfaces", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "input": { + "historical": true, + "path": "refactor/baselines/site-vendor/console-original.js", + "sha256": "e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03" + }, + "pageErrors": [] + }, + { + "title": "historical console renders levels and raw objects; Clear resets the visible log count", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "input": { + "historical": true, + "path": "refactor/baselines/site-vendor/console-original.js", + "sha256": "e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03" + }, + "pageErrors": [] + }, + { + "title": "historical console scrolls new logs and preserves the pointer hover pause", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "input": { + "historical": true, + "path": "refactor/baselines/site-vendor/console-original.js", + "sha256": "e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03" + }, + "pageErrors": [] + }, + { + "title": "frozen historical console leaves its scheduled scroll callback after unmount", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "input": { + "historical": true, + "path": "refactor/baselines/site-vendor/console-original.js", + "sha256": "e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03" + }, + "pageErrors": [] + }, + { + "title": "frozen historical console loses the remaining viewer when another viewer unmounts", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "input": { + "historical": true, + "path": "refactor/baselines/site-vendor/console-original.js", + "sha256": "e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03" + }, + "pageErrors": [] + }, + { + "title": "frozen historical console error-only rendering loses the message on Firefox and WebKit", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "input": { + "historical": true, + "path": "refactor/baselines/site-vendor/console-original.js", + "sha256": "e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03" + }, + "pageErrors": [] + }, + { + "title": "candidate console cancels pending scroll and log work on unmount and survives remount", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "input": { + "historical": false, + "path": "docs/assets/js/console.js", + "sha256": "5644af7bb35bb6d0bcfa7eeae9a21b406ac4fc1b499a6c797a6534c7d33f0678" + }, + "pageErrors": [] + }, + { + "title": "candidate console retains the surviving viewer when removing first", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "input": { + "historical": false, + "path": "docs/assets/js/console.js", + "sha256": "5644af7bb35bb6d0bcfa7eeae9a21b406ac4fc1b499a6c797a6534c7d33f0678" + }, + "pageErrors": [] + }, + { + "title": "candidate console retains the surviving viewer when removing second", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "input": { + "historical": false, + "path": "docs/assets/js/console.js", + "sha256": "5644af7bb35bb6d0bcfa7eeae9a21b406ac4fc1b499a6c797a6534c7d33f0678" + }, + "pageErrors": [] + }, + { + "title": "candidate console keeps native Error message and stack visible without modifying Error", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "input": { + "historical": false, + "path": "docs/assets/js/console.js", + "sha256": "5644af7bb35bb6d0bcfa7eeae9a21b406ac4fc1b499a6c797a6534c7d33f0678" + }, + "pageErrors": [] + }, + { + "title": "candidate console leaves later external wrappers intact and remounts without duplicate forwarding", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "input": { + "historical": false, + "path": "docs/assets/js/console.js", + "sha256": "5644af7bb35bb6d0bcfa7eeae9a21b406ac4fc1b499a6c797a6534c7d33f0678" + }, + "pageErrors": [] + }, + { + "title": "editor retains console on persisted pagehide and releases it on final pagehide", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "input": null, + "pageErrors": [] + }, + { + "title": "current console globals, returned component and repeated mount retain their interfaces", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "input": { + "historical": false, + "path": "docs/assets/js/console.js", + "sha256": "5644af7bb35bb6d0bcfa7eeae9a21b406ac4fc1b499a6c797a6534c7d33f0678" + }, + "pageErrors": [] + }, + { + "title": "current console renders levels and raw objects; Clear resets the visible log count", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "input": { + "historical": false, + "path": "docs/assets/js/console.js", + "sha256": "5644af7bb35bb6d0bcfa7eeae9a21b406ac4fc1b499a6c797a6534c7d33f0678" + }, + "pageErrors": [] + }, + { + "title": "current console scrolls new logs and preserves the pointer hover pause", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "input": { + "historical": false, + "path": "docs/assets/js/console.js", + "sha256": "5644af7bb35bb6d0bcfa7eeae9a21b406ac4fc1b499a6c797a6534c7d33f0678" + }, + "pageErrors": [] + }, + { + "title": "historical console globals, returned component and repeated mount retain their interfaces", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "input": { + "historical": true, + "path": "refactor/baselines/site-vendor/console-original.js", + "sha256": "e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03" + }, + "pageErrors": [] + }, + { + "title": "historical console renders levels and raw objects; Clear resets the visible log count", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "input": { + "historical": true, + "path": "refactor/baselines/site-vendor/console-original.js", + "sha256": "e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03" + }, + "pageErrors": [] + }, + { + "title": "historical console scrolls new logs and preserves the pointer hover pause", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "input": { + "historical": true, + "path": "refactor/baselines/site-vendor/console-original.js", + "sha256": "e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03" + }, + "pageErrors": [] + }, + { + "title": "frozen historical console leaves its scheduled scroll callback after unmount", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "input": { + "historical": true, + "path": "refactor/baselines/site-vendor/console-original.js", + "sha256": "e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03" + }, + "pageErrors": [] + }, + { + "title": "frozen historical console loses the remaining viewer when another viewer unmounts", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "input": { + "historical": true, + "path": "refactor/baselines/site-vendor/console-original.js", + "sha256": "e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03" + }, + "pageErrors": [] + }, + { + "title": "frozen historical console error-only rendering loses the message on Firefox and WebKit", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "input": { + "historical": true, + "path": "refactor/baselines/site-vendor/console-original.js", + "sha256": "e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03" + }, + "pageErrors": [] + }, + { + "title": "candidate console cancels pending scroll and log work on unmount and survives remount", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "input": { + "historical": false, + "path": "docs/assets/js/console.js", + "sha256": "5644af7bb35bb6d0bcfa7eeae9a21b406ac4fc1b499a6c797a6534c7d33f0678" + }, + "pageErrors": [] + }, + { + "title": "candidate console retains the surviving viewer when removing first", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "input": { + "historical": false, + "path": "docs/assets/js/console.js", + "sha256": "5644af7bb35bb6d0bcfa7eeae9a21b406ac4fc1b499a6c797a6534c7d33f0678" + }, + "pageErrors": [] + }, + { + "title": "candidate console retains the surviving viewer when removing second", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "input": { + "historical": false, + "path": "docs/assets/js/console.js", + "sha256": "5644af7bb35bb6d0bcfa7eeae9a21b406ac4fc1b499a6c797a6534c7d33f0678" + }, + "pageErrors": [] + }, + { + "title": "candidate console keeps native Error message and stack visible without modifying Error", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "input": { + "historical": false, + "path": "docs/assets/js/console.js", + "sha256": "5644af7bb35bb6d0bcfa7eeae9a21b406ac4fc1b499a6c797a6534c7d33f0678" + }, + "pageErrors": [] + }, + { + "title": "candidate console leaves later external wrappers intact and remounts without duplicate forwarding", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "input": { + "historical": false, + "path": "docs/assets/js/console.js", + "sha256": "5644af7bb35bb6d0bcfa7eeae9a21b406ac4fc1b499a6c797a6534c7d33f0678" + }, + "pageErrors": [] + }, + { + "title": "editor retains console on persisted pagehide and releases it on final pagehide", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "input": null, + "pageErrors": [] + }, + { + "title": "current console globals, returned component and repeated mount retain their interfaces", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "input": { + "historical": false, + "path": "docs/assets/js/console.js", + "sha256": "5644af7bb35bb6d0bcfa7eeae9a21b406ac4fc1b499a6c797a6534c7d33f0678" + }, + "pageErrors": [] + }, + { + "title": "current console renders levels and raw objects; Clear resets the visible log count", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "input": { + "historical": false, + "path": "docs/assets/js/console.js", + "sha256": "5644af7bb35bb6d0bcfa7eeae9a21b406ac4fc1b499a6c797a6534c7d33f0678" + }, + "pageErrors": [] + }, + { + "title": "current console scrolls new logs and preserves the pointer hover pause", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "input": { + "historical": false, + "path": "docs/assets/js/console.js", + "sha256": "5644af7bb35bb6d0bcfa7eeae9a21b406ac4fc1b499a6c797a6534c7d33f0678" + }, + "pageErrors": [] + }, + { + "title": "historical console globals, returned component and repeated mount retain their interfaces", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "input": { + "historical": true, + "path": "refactor/baselines/site-vendor/console-original.js", + "sha256": "e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03" + }, + "pageErrors": [] + }, + { + "title": "historical console renders levels and raw objects; Clear resets the visible log count", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "input": { + "historical": true, + "path": "refactor/baselines/site-vendor/console-original.js", + "sha256": "e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03" + }, + "pageErrors": [] + }, + { + "title": "historical console scrolls new logs and preserves the pointer hover pause", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "input": { + "historical": true, + "path": "refactor/baselines/site-vendor/console-original.js", + "sha256": "e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03" + }, + "pageErrors": [] + }, + { + "title": "frozen historical console leaves its scheduled scroll callback after unmount", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "input": { + "historical": true, + "path": "refactor/baselines/site-vendor/console-original.js", + "sha256": "e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03" + }, + "pageErrors": [] + }, + { + "title": "frozen historical console loses the remaining viewer when another viewer unmounts", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "input": { + "historical": true, + "path": "refactor/baselines/site-vendor/console-original.js", + "sha256": "e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03" + }, + "pageErrors": [] + }, + { + "title": "frozen historical console error-only rendering loses the message on Firefox and WebKit", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "input": { + "historical": true, + "path": "refactor/baselines/site-vendor/console-original.js", + "sha256": "e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03" + }, + "pageErrors": [] + }, + { + "title": "candidate console cancels pending scroll and log work on unmount and survives remount", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "input": { + "historical": false, + "path": "docs/assets/js/console.js", + "sha256": "5644af7bb35bb6d0bcfa7eeae9a21b406ac4fc1b499a6c797a6534c7d33f0678" + }, + "pageErrors": [] + }, + { + "title": "candidate console retains the surviving viewer when removing first", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "input": { + "historical": false, + "path": "docs/assets/js/console.js", + "sha256": "5644af7bb35bb6d0bcfa7eeae9a21b406ac4fc1b499a6c797a6534c7d33f0678" + }, + "pageErrors": [] + }, + { + "title": "candidate console retains the surviving viewer when removing second", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "input": { + "historical": false, + "path": "docs/assets/js/console.js", + "sha256": "5644af7bb35bb6d0bcfa7eeae9a21b406ac4fc1b499a6c797a6534c7d33f0678" + }, + "pageErrors": [] + }, + { + "title": "candidate console keeps native Error message and stack visible without modifying Error", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "input": { + "historical": false, + "path": "docs/assets/js/console.js", + "sha256": "5644af7bb35bb6d0bcfa7eeae9a21b406ac4fc1b499a6c797a6534c7d33f0678" + }, + "pageErrors": [] + }, + { + "title": "candidate console leaves later external wrappers intact and remounts without duplicate forwarding", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "input": { + "historical": false, + "path": "docs/assets/js/console.js", + "sha256": "5644af7bb35bb6d0bcfa7eeae9a21b406ac4fc1b499a6c797a6534c7d33f0678" + }, + "pageErrors": [] + }, + { + "title": "editor retains console on persisted pagehide and releases it on final pagehide", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "input": null, + "pageErrors": [] + }, + { + "title": "desktop TypeScript mode emits and runs typed code, Ctrl-S reruns and invalid syntax keeps the current player", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "input": null, + "pageErrors": [] + }, + { + "title": "desktop file imports retain order, support CSS and remain usable after Run", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "input": null, + "pageErrors": [] + }, + { + "title": "denied browser storage falls back to a working editor and reports unsaved preferences", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "input": null, + "pageErrors": [] + }, + { + "title": "stored production and visibility settings reload with their existing keys", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "input": null, + "pageErrors": [] + }, + { + "title": "Monaco language AMD modules finish before example dependencies can suspend define", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "input": null, + "pageErrors": [] + }, + { + "title": "failed declaration loading disposes partial models and prevents premature Run", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "input": null, + "pageErrors": [] + }, + { + "title": "desktop TypeScript mode emits and runs typed code, Ctrl-S reruns and invalid syntax keeps the current player", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "input": null, + "pageErrors": [] + }, + { + "title": "desktop file imports retain order, support CSS and remain usable after Run", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "input": null, + "pageErrors": [] + }, + { + "title": "denied browser storage falls back to a working editor and reports unsaved preferences", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "input": null, + "pageErrors": [] + }, + { + "title": "stored production and visibility settings reload with their existing keys", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "input": null, + "pageErrors": [] + }, + { + "title": "Monaco language AMD modules finish before example dependencies can suspend define", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "input": null, + "pageErrors": [] + }, + { + "title": "failed declaration loading disposes partial models and prevents premature Run", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "input": null, + "pageErrors": [] + }, + { + "title": "desktop TypeScript mode emits and runs typed code, Ctrl-S reruns and invalid syntax keeps the current player", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "input": null, + "pageErrors": [] + }, + { + "title": "desktop file imports retain order, support CSS and remain usable after Run", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "input": null, + "pageErrors": [] + }, + { + "title": "denied browser storage falls back to a working editor and reports unsaved preferences", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "input": null, + "pageErrors": [] + }, + { + "title": "stored production and visibility settings reload with their existing keys", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "input": null, + "pageErrors": [] + }, + { + "title": "Monaco language AMD modules finish before example dependencies can suspend define", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "input": null, + "pageErrors": [] + }, + { + "title": "failed declaration loading disposes partial models and prevents premature Run", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "input": null, + "pageErrors": [] + } + ] + }, + "statefulBrowser": { + "path": "refactor/.cache/console01-stateful-report/report.json", + "stats": { + "startTime": "2026-09-14T22:10:25.091Z", + "duration": 7250.746999999999, + "expected": 3, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "cases": [ + { + "title": "candidate console parses stateful methods once for shared viewers", + "project": "chromium", + "status": "passed", + "retry": 0, + "browser": "153.0.8010.12", + "platform": "win32", + "input": { + "historical": false, + "path": "docs/assets/js/console.js", + "sha256": "5644af7bb35bb6d0bcfa7eeae9a21b406ac4fc1b499a6c797a6534c7d33f0678" + }, + "pageErrors": [] + }, + { + "title": "candidate console parses stateful methods once for shared viewers", + "project": "firefox", + "status": "passed", + "retry": 0, + "browser": "155.0", + "platform": "win32", + "input": { + "historical": false, + "path": "docs/assets/js/console.js", + "sha256": "5644af7bb35bb6d0bcfa7eeae9a21b406ac4fc1b499a6c797a6534c7d33f0678" + }, + "pageErrors": [] + }, + { + "title": "candidate console parses stateful methods once for shared viewers", + "project": "webkit", + "status": "passed", + "retry": 0, + "browser": "26.6", + "platform": "win32", + "input": { + "historical": false, + "path": "docs/assets/js/console.js", + "sha256": "5644af7bb35bb6d0bcfa7eeae9a21b406ac4fc1b499a6c797a6534c7d33f0678" + }, + "pageErrors": [] + } + ] + }, + "checks": { + "units": { + "command": "node --test test/site-console.test.js", + "passed": 6, + "failed": 0, + "log": "refactor/.cache/console01-final-units.log" + }, + "relatedUnits": { + "command": "node --test test/site-editor.test.js test/site-notices.test.js", + "passed": 10, + "failed": 0, + "log": "refactor/.cache/console01-related-units.log" + }, + "strictTypes": [ + "scripts/tsconfig.docs.json", + "packages/artplayer-vitepress/browser/tsconfig.json" + ], + "rootLint": { + "exitCode": 0, + "eslintErrors": 0, + "eslintWarnings": 0, + "log": "refactor/.cache/console01-root-lint.log", + "note": "Yarn emits its pre-existing Node url.parse deprecation warning." + }, + "buildAndDrift": [ + "build:console", + "check:console", + "build:site-assets", + "check:site-assets" + ], + "ciConfiguration": "check:ci passed locally; no remote run", + "strictToolchain": "Node 24.21.0, Yarn 1.22.22; 22 workspaces, 40 pinned tools, 1472 selectors verified" + }, + "limits": [ + "Historical defect cases intentionally pass by reproducing old failures.", + "63 final combined cases plus 3 stateful cases: no failures, skips or retries.", + "Persisted pagehide is a controlled event; not physical BFCache/device certification.", + "No console vendor provenance/license closure; VENDOR-08 remains open.", + "No full ecosystem, remote CI, deployment or npm publication acceptance." + ] +} diff --git a/refactor/changes/2026-09-15-SITE-CONSOLE-01-typescript.md b/refactor/changes/2026-09-15-SITE-CONSOLE-01-typescript.md new file mode 100644 index 000000000..6cca39be4 --- /dev/null +++ b/refactor/changes/2026-09-15-SITE-CONSOLE-01-typescript.md @@ -0,0 +1,48 @@ +# SITE-CONSOLE-01 控制台自有 TS 与生命周期修复 + +旧控制台为不可维护的 Parcel bundle;两个视图会争夺同一个 console hook,卸载后 +滚动仍触碰旧 DOM,Firefox/WebKit 的原生错误消息也会被旧 parser 丢弃。 +本任务恢复自有 TS 模块并修复这些已复现行为,保留其余固定第三方运行时。 + +## 结构与兼容 + +模块职责、队列与资源所有权见 +[维护说明](../../scripts/site-vendor/console/README.md)。build.ts 验证原始 SHA-256, +用既有 esbuild 0.27.7 编译入口和视图,仅替换 Focm/W5CS 函数体并移除失效 map +注释。其他 100 模块、依赖表、Parcel 包装及旧 URL 保留。CSS 字符串与旧版精确 +对照。React/ReactDOM 17.0.2、styled-components 5.3.3 继续从原模块注入;根工程 +React 类型仅用于检查,不携带 React 19 运行时。TypeScript 5.9.3 严格检查边界。 + +涉及 API-02/03/05/08/09:consoleLog 仍返回 React 组件,重复挂载复用;实例方法、 +状态、对象检查、等级/计数/Clear/悬停滚动及全局加载行为保留。新增可选 unmount +适配器,编辑器 pagehide 统一清理。共享 owner 使 count/time 只解析一次,已卸载 +订阅不再收到排队日志。最后卸载恢复原属性描述符,且不覆盖后来安装的外部 wrapper。 +错误消息适配保留堆栈且不修改 Error。回调抛错继续对外可见,同时不阻断其他视图。 + +这属于站点内部修复,没有库入口或包版本变化。VENDOR-08 只更新候选指纹与修改 +边界;未知的完整第三方版本/源码与许可保持开放,不以冻结 bundle 宣称来源完整。 + +## 验证 + +- 先用正确行为回归验证旧生产脚本:11 失败/1 通过,保留原报告。 +- 初次候选组合 57/57;完成编辑器清理后的最终组合 63/63,追加共享状态解析 3/3。 + Chromium 153.0.8010.12、Firefox 155.0、WebKit 26.6,Windows,零重试/跳过。 +- 六项单元覆盖原生转发/接收者、共享解析、过期日志、失败回滚、外部 wrapper、 + 抛错订阅、错误对象、样式与两个模块以外的字节保护;编辑器和 notice 十项通过。 +- 两组严格 TS、生成/check、根 lint 通过;追加测试 scoped lint 通过。 + 根 lint 的 ESLint 零错误/警告,Yarn 仍输出已有 Node url.parse 弃用提示。 +- CI 配置检查通过;impact 采取保守全生态映射,并继续报告未完成的安装消费缺口。 + +原始失败、首轮成功、最终输入/逐项结果见 +[验证记录](../baselines/site-console01-validation.json)。受控 pagehide 测试不等于 +真实物理设备 BFCache;没有宣称全生态测试、远端 CI、部署或 npm 发布完成。 + +## 工程入口与回退 + +新增 build:console、check:console、test:site-console,并接入 ci:build、ci:check、 +test:node。复用已安装依赖与根 yarn.lock;没有新增依赖或改变 Yarn Classic 1.22.22 / +Node 24.21.0。源码和包内维护说明与状态同次提交。 + +回退本任务 commit,恢复原控制台、编辑器清理和生成/检查入口,同时恢复风险与 +候选指纹;冻结历史不动。不要手工改生成 bundle。下一步继续 SITE-07 的 console +第三方溯源/许可和站点剩余资产,SITE-05 仍需完整站点验收。无推送或发布。 diff --git a/refactor/console-modernization.md b/refactor/console-modernization.md index 8d5865760..73463cb82 100644 --- a/refactor/console-modernization.md +++ b/refactor/console-modernization.md @@ -1,6 +1,6 @@ # 文档站 console.js 的兼容迁移边界 -SITE-07 先冻结原始 bundle 和浏览器契约,后续实现由 SITE-CONSOLE-01 负责。 +SITE-07 冻结原始 bundle 和浏览器契约,SITE-CONSOLE-01 已实现自有 TS 模块及三项修复。 不能将 consoleLog 替换成一个只打印字符串的新面板,就声称旧接口已兼容。 ## 已核实的现状 @@ -30,7 +30,7 @@ SITE-07 先冻结原始 bundle 和浏览器契约,后续实现由 SITE-CONSOLE 冻结文件 [console-original.js](baselines/site-vendor/console-original.js) 仅作为旧行为 对照,不进入站点分发。测试校验其 SHA-256;后续不得覆盖旧文件来制造差分通过。 -## 已复现、尚未修复的问题 +## 旧版已复现、候选已修复的问题 1. **CONSOLE-SCROLL-01**:add 安排 200ms 滚动回调;卸载后未取消,回调仍访问已移除 视图。使用真实定时器、原 ReactDOM 卸载和 DOM 验证,三个引擎均可复现。 @@ -40,7 +40,7 @@ SITE-07 先冻结原始 bundle 和浏览器契约,后续实现由 SITE-CONSOLE WebKit 的原生 Error.stack 不含 message,界面丢失消息。Chromium 为有消息的对照。 普通对象保留引用,但 Error 已被旧 parser 转成字符串,不能误称仍保留 Error 身份。 -## 实施顺序 +## 已执行的实施顺序 1. 从已冻结自有入口/视图恢复可维护 TS,拆开安装、日志订阅/状态、视图与资源清理。 结合固定第三方边界建立构建/check,保留旧入口和已观测全局,不引入 React 版本混用。 @@ -53,6 +53,29 @@ SITE-07 先冻结原始 bundle 和浏览器契约,后续实现由 SITE-CONSOLE 5. SITE-07 继续完成全部第三方来源/版本与完整许可。来源未确认时不得关闭 VENDOR-08; 旧 vendor 的冻结不等于许可审查通过,也不等于完成其可复现来源恢复。 +## 实施结果与边界 + +安装、组件、共享订阅、错误适配、样式、类型和构建分别维护,见 +[模块地图](../scripts/site-vendor/console/README.md)。构建仅替换 Focm/W5CS 的函数体, +保留其余 100 模块、依赖表和 Parcel runtime;移除无实际文件的 sourceMappingURL。 +样式解码值与旧模块逐字比较,React/ReactDOM 仍为 17.0.2。没有新增依赖。 + +单一 hook owner 在多个视图间共享计数/计时解析,最后卸载时取消队列并有条件恢复 +原方法;外部后来安装的 wrapper 保留。滚动回调由组件持有并在卸载时取消。 +原生 Error 缺少消息头时适配显示,保留完整堆栈且不修改对象;普通对象仍保持引用。 +新增可选 consoleLog.unmount 供编辑器非 persisted pagehide 清理,原调用返回组件 +及重复挂载行为保留。旧的 command/result 不存在时,卸载恢复为不存在,不遗留 undefined +自有属性;这属于方法描述符清理修正,不为这些非原生方法新增调用能力。 + +候选回归在旧生产脚本上先得到 11 失败/1 通过(Chromium 错误消息原已正常)。 +修复后首轮 57 通过;补编辑器清理后最终组合 63 通过,另加共享状态解析 3 通过, +均无跳过或重试。六项单元及十项编辑器/notice 测试通过,两组严格 TS 和生成漂移 +检查通过。完整输入指纹、引擎版本和结果见 +[验证记录](baselines/site-console01-validation.json)。 + +受控 persisted pagehide 事件不代替真实设备 BFCache;第三方来源/许可仍由 +SITE-07 / VENDOR-08 跟踪。本次没有远端 CI、部署或 npm 发布验收。 + 当前 [site-console.spec.js](../test/browser/site-console.spec.js) 含两种输入的共同 行为,以及三个仅指向冻结旧版的问题复现。历史问题用例通过意味着问题仍可复现, **不意味着候选已修复**。SITE-CONSOLE-01 必须新增候选“无泄漏/不丢日志/错误消息可见” diff --git a/refactor/plan.md b/refactor/plan.md index fe6f6271d..795dd3b41 100644 --- a/refactor/plan.md +++ b/refactor/plan.md @@ -4,7 +4,7 @@ 基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 265 项,范围 22 个包及工作区/示例。 -状态:todo 45 / doing 22 / blocked 0 / done 198 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 +状态:todo 44 / doing 22 / blocked 0 / done 199 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。 @@ -410,7 +410,7 @@ | EX-02 | example/vue.js, workspace
验证 Vue 消费者与更新卸载 | CORE-22, ENG-07 | Vue 实例/ref、参数更新、卸载及插件样例 | 旧 JS 组件用法无需修改,重复挂载不泄漏 | M | done | | EX-03 | workspace
验证全部原生 demo 与外部播放集成 | EX-01, EX-02, PKG-CHAPTER-06, PKG-AMBILIGHT-06, PKG-AUDIO-06, PKG-AUTO-THUMB-06, PKG-VTT-THUMB-06, PKG-HLS-06, PKG-DASH-06, PKG-MULTI-SUB-06, PKG-JASSUB-06, PKG-MASK-06, PKG-ASR-06, PKG-ADS-06, PKG-VAST-06, PKG-CAST-06, PKG-DPIP-06, PKG-DANMUKU-09, PKG-CANVAS-06, PKG-MB-10, PKG-IFRAME-06, PKG-TOOL-THUMB-06 | 8082 全 demo、HLS/DASH/FLV/MPEGTS/WebTorrent 集成记录 | 旧 URL/参数/脚本加载保持;网络/SDK 限制明确,不静默跳过;覆盖编辑器重复 Run 和 ESM/i18n/mobile/iframe,记录实际候选脚本及状态隔离证据;依据 BASE-04 路径台账逐项消除 not-run,处理 BASE-DEMO-01 | H | todo | | SITE-VCONSOLE-01 | artplayer-vitepress, workspace
修复 vConsole 日志帧与延迟面板销毁竞态 | SITE-01 | 冻结上游 bundle 的可复现 TS 生命周期补丁、旧红新绿回归、来源及生成检查;销毁后虚拟列表布局续段停止 | 最后日志插件移除后取消 RAF 并隔离旧队列;延迟面板不得写入已销毁或替换插件;三引擎移动页与 UMD 入口通过,维护声明与原有许可缺口分别记录;原生 ResizeObserver 布局与挂起 timer 旧红新绿 | M | done | -| SITE-CONSOLE-01 | artplayer-vitepress, workspace
迁移控制台自有 TS 源码并修复日志生命周期 | SITE-03 | 从冻结 console.js 恢复自有入口/视图的 TS 模块、受控第三方边界与可验证构建;修复滚动回调、多容器 hook 所有权和原生 Error 消息丢失 | 保留 consoleLog 返回组件、React/ReactDOM/Parcel 全局及 DOM/CSS/日志对象语义;旧版复现与候选正确行为分开验证;多容器、卸载/重挂、外部 hook、错误/滚动及真实编辑器组合通过;架构与生成文档同批更新,不以冻结 vendor 代替 SITE-07 许可闭环 | H | todo | +| SITE-CONSOLE-01 | artplayer-vitepress, workspace
迁移控制台自有 TS 源码并修复日志生命周期 | SITE-03 | 从冻结 console.js 恢复自有入口/视图的 TS 模块、受控第三方边界与可验证构建;修复滚动回调、多容器 hook 所有权和原生 Error 消息丢失 | 保留 consoleLog 返回组件、React/ReactDOM/Parcel 全局及 DOM/CSS/日志对象语义;旧版复现与候选正确行为分开验证;多容器、卸载/重挂、外部 hook、错误/滚动及真实编辑器组合通过;架构与生成文档同批更新,不以冻结 vendor 代替 SITE-07 许可闭环 | H | done | ## 7 工具链与性能 @@ -694,4 +694,4 @@ - PKG-DANMUKU-MASK-LOAD-01: [记录](changes/2026-09-14-PKG-DANMUKU-08-combined-load.md) [记录](baselines/danmuku-combined-load-validation.json) [记录](changes/2026-09-14-PKG-DANMUKU-MASK-LOAD-01-scheduling.md) [记录](baselines/danmuku-mask-load-validation.json) - PKG-HLS-PACK-01: [记录](changes/2026-09-15-PKG-HLS-PACK-01-config.md) [记录](baselines/hls-pack-config-validation.json) - ENG-LINT-02: [记录](changes/2026-09-15-ENG-LINT-02-clean-checks.md) [记录](baselines/eng-lint02-validation.json) -- SITE-CONSOLE-01: [记录](console-modernization.md) [记录](baselines/site-console-validation.json) +- SITE-CONSOLE-01: [记录](console-modernization.md) [记录](changes/2026-09-15-SITE-CONSOLE-01-typescript.md) [记录](baselines/site-console01-validation.json) diff --git a/refactor/progress.md b/refactor/progress.md index 52f35e697..cbccc4ba9 100644 --- a/refactor/progress.md +++ b/refactor/progress.md @@ -1,5 +1,13 @@ # 进度与证据 +## SITE-CONSOLE-01 自有 TS 控制台与三项修复 + +完成入口/视图/订阅/错误/样式/类型拆分,保留原 React17 与 100 个第三方模块。 +多面板 hook、卸载滚动、错误消息三项已复现风险已修复,编辑器 pagehide 清理一并 +接入。最终组合 63/63、共享解析 3/3,单元 6+10 通过;详见 +[记录](changes/2026-09-15-SITE-CONSOLE-01-typescript.md)。199/265 项完成。 +SITE-07 / VENDOR-08 的完整源码及许可仍开放;下一步继续该边界,未推送或发布。 + ## SITE-07 控制台基线与三项缺陷 冻结 102 模块和真实全局/返回组件/DOM/日志/滚动边界。三引擎最终27/27为18项共同 diff --git a/refactor/risk-table.md b/refactor/risk-table.md index d263d91a5..8dcebdcc3 100644 --- a/refactor/risk-table.md +++ b/refactor/risk-table.md @@ -276,6 +276,6 @@ | DPIP-INSTALLED-PROVENANCE-01 | resolved / 已复现 | Document PiP测试加载器忽略显式安装map并回退到源码 | PKG-CANVAS-05, CI-01 | | DASH-MENU-01 | resolved / 已复现 | SDK 5.2.1 播放中设置质量选项在点击时变为不可见 | PKG-DASH-MENU-01, PKG-DASH-05, REVIEW-02 | | DANMUKU-MASK-LOAD-01 | resolved / 已复现 | 真实Mask模型运行时弹幕存在未采样窗口及串行放置积压 | PKG-DANMUKU-MASK-LOAD-01, PKG-DANMUKU-08, PKG-MASK-05 | -| CONSOLE-SCROLL-01 | open / 已复现 | 控制台卸载后仍执行滚动回调 | SITE-CONSOLE-01, SITE-07 | -| CONSOLE-OWNERSHIP-01 | open / 已复现 | 多个控制台的全局 hook 所有权冲突 | SITE-CONSOLE-01, SITE-07 | -| CONSOLE-ERROR-01 | open / 已复现 | Firefox/WebKit 控制台丢失原生 Error 消息 | SITE-CONSOLE-01, SITE-07 | +| CONSOLE-SCROLL-01 | resolved / 已复现 | 控制台卸载后仍执行滚动回调 | SITE-CONSOLE-01, SITE-07 | +| CONSOLE-OWNERSHIP-01 | resolved / 已复现 | 多个控制台的全局 hook 所有权冲突 | SITE-CONSOLE-01, SITE-07 | +| CONSOLE-ERROR-01 | resolved / 已复现 | Firefox/WebKit 控制台丢失原生 Error 消息 | SITE-CONSOLE-01, SITE-07 | diff --git a/refactor/risks.json b/refactor/risks.json index cde0860dd..25ea929cc 100644 --- a/refactor/risks.json +++ b/refactor/risks.json @@ -6396,7 +6396,7 @@ "id": "CONSOLE-SCROLL-01", "title": "控制台卸载后仍执行滚动回调", "confirmation": "reproduced", - "status": "open", + "status": "resolved", "owners": [ "SITE-CONSOLE-01", "SITE-07" @@ -6408,15 +6408,23 @@ "refactor/changes/2026-09-15-SITE-07-console-baseline.md", "test/browser/site-console.spec.js" ], - "workspaceState": "Native 200ms scroll timeout survives ReactDOM unmount and later fires against the detached console node.", + "workspaceState": "Owned TS console lifecycle and error adaptation implemented; frozen historical reproduction and candidate regression remain separate.", "compatibleResolution": "Cancel pending scroll work and guard obsolete log updates; verify unmount/recreate without retained DOM work.", - "closureCriteria": "新候选修复和真实浏览器回归通过,冻结旧版仍可复现;同时验证原控制台接口和资源释放,不把历史缺陷测试通过当作修复。" + "closureCriteria": "新候选修复和真实浏览器回归通过,冻结旧版仍可复现;同时验证原控制台接口和资源释放,不把历史缺陷测试通过当作修复。", + "resolutionEvidence": [ + "refactor/changes/2026-09-15-SITE-CONSOLE-01-typescript.md", + "refactor/baselines/site-console01-validation.json", + "test/browser/site-console.spec.js", + "test/site-console.test.js", + "scripts/site-vendor/console/README.md" + ], + "resolutionRationale": "Candidate behavior failed on the original bundle (11 failed/1 passed); final browser/editor matrix 63 passed plus shared parser 3 passed across Chromium/Firefox/WebKit. Units, strict types and generation checks pass. Preserves React17, old component return, raw objects, exact CSS and 100 vendor module bodies. Physical BFCache and complete third-party provenance remain separate." }, { "id": "CONSOLE-OWNERSHIP-01", "title": "多个控制台的全局 hook 所有权冲突", "confirmation": "reproduced", - "status": "open", + "status": "resolved", "owners": [ "SITE-CONSOLE-01", "SITE-07" @@ -6428,15 +6436,23 @@ "refactor/changes/2026-09-15-SITE-07-console-baseline.md", "test/browser/site-console.spec.js" ], - "workspaceState": "Unmounting the first of two viewers unhooks the second viewer and restores a stale first-viewer wrapper; the surviving viewer no longer receives logs.", + "workspaceState": "Owned TS console lifecycle and error adaptation implemented; frozen historical reproduction and candidate regression remain separate.", "compatibleResolution": "Own shared subscriptions explicitly, preserve unrelated wrappers, and test both unmount orders plus same-container reuse.", - "closureCriteria": "新候选修复和真实浏览器回归通过,冻结旧版仍可复现;同时验证原控制台接口和资源释放,不把历史缺陷测试通过当作修复。" + "closureCriteria": "新候选修复和真实浏览器回归通过,冻结旧版仍可复现;同时验证原控制台接口和资源释放,不把历史缺陷测试通过当作修复。", + "resolutionEvidence": [ + "refactor/changes/2026-09-15-SITE-CONSOLE-01-typescript.md", + "refactor/baselines/site-console01-validation.json", + "test/browser/site-console.spec.js", + "test/site-console.test.js", + "scripts/site-vendor/console/README.md" + ], + "resolutionRationale": "Candidate behavior failed on the original bundle (11 failed/1 passed); final browser/editor matrix 63 passed plus shared parser 3 passed across Chromium/Firefox/WebKit. Units, strict types and generation checks pass. Preserves React17, old component return, raw objects, exact CSS and 100 vendor module bodies. Physical BFCache and complete third-party provenance remain separate." }, { "id": "CONSOLE-ERROR-01", "title": "Firefox/WebKit 控制台丢失原生 Error 消息", "confirmation": "reproduced", - "status": "open", + "status": "resolved", "owners": [ "SITE-CONSOLE-01", "SITE-07" @@ -6448,9 +6464,17 @@ "refactor/changes/2026-09-15-SITE-07-console-baseline.md", "test/browser/site-console.spec.js" ], - "workspaceState": "The bundled error parser converts Error to stack; native Firefox/WebKit stacks omit message, so error-only logs lose their message. Chromium is a positive contrast.", + "workspaceState": "Owned TS console lifecycle and error adaptation implemented; frozen historical reproduction and candidate regression remain separate.", "compatibleResolution": "Preserve error message and stack in the candidate without mutating ordinary log objects; require actual three-engine Error-only rendering tests.", - "closureCriteria": "新候选修复和真实浏览器回归通过,冻结旧版仍可复现;同时验证原控制台接口和资源释放,不把历史缺陷测试通过当作修复。" + "closureCriteria": "新候选修复和真实浏览器回归通过,冻结旧版仍可复现;同时验证原控制台接口和资源释放,不把历史缺陷测试通过当作修复。", + "resolutionEvidence": [ + "refactor/changes/2026-09-15-SITE-CONSOLE-01-typescript.md", + "refactor/baselines/site-console01-validation.json", + "test/browser/site-console.spec.js", + "test/site-console.test.js", + "scripts/site-vendor/console/README.md" + ], + "resolutionRationale": "Candidate behavior failed on the original bundle (11 failed/1 passed); final browser/editor matrix 63 passed plus shared parser 3 passed across Chromium/Firefox/WebKit. Units, strict types and generation checks pass. Preserves React17, old component return, raw objects, exact CSS and 100 vendor module bodies. Physical BFCache and complete third-party provenance remain separate." } ] } diff --git a/refactor/tasks.json b/refactor/tasks.json index 1a4fabc87..5bccd652a 100644 --- a/refactor/tasks.json +++ b/refactor/tasks.json @@ -5612,13 +5612,14 @@ "dependsOn": [ "SITE-03" ], - "status": "todo", + "status": "done", "risk": "H", "deliverable": "从冻结 console.js 恢复自有入口/视图的 TS 模块、受控第三方边界与可验证构建;修复滚动回调、多容器 hook 所有权和原生 Error 消息丢失", "acceptance": "保留 consoleLog 返回组件、React/ReactDOM/Parcel 全局及 DOM/CSS/日志对象语义;旧版复现与候选正确行为分开验证;多容器、卸载/重挂、外部 hook、错误/滚动及真实编辑器组合通过;架构与生成文档同批更新,不以冻结 vendor 代替 SITE-07 许可闭环", "evidence": [ "console-modernization.md", - "baselines/site-console-validation.json" + "changes/2026-09-15-SITE-CONSOLE-01-typescript.md", + "baselines/site-console01-validation.json" ] } ] diff --git a/refactor/third-party.json b/refactor/third-party.json index ba8059d49..068dcfa97 100644 --- a/refactor/third-party.json +++ b/refactor/third-party.json @@ -1060,7 +1060,7 @@ "owners": [ "SITE-01" ], - "sourceStatus": "Frozen original 340306-byte Parcel bundle e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03; 102-module map, Focm entry/W5CS view, React 17.0.2 globals and styled-components 5.3.3 literal identified. Full dependency versions and source reconstruction remain open.", + "sourceStatus": "Frozen original 340306-byte Parcel bundle e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03; 102-module map, Focm entry/W5CS view, React 17.0.2 globals and styled-components 5.3.3 literal identified. Full dependency versions and source reconstruction remain open. SITE-CONSOLE-01 replaces only Focm/W5CS bodies from owned TS and removes the missing source-map trailer; other 100 modules and runtime remain byte-for-byte frozen. Full vendor provenance remains open.", "licenseStatus": "Root MIT is not sufficient evidence for all bundled components; inventory and notices pending.", "upstreamSources": [], "upstreamReviewedAt": null, @@ -1069,7 +1069,7 @@ { "path": "docs/assets/js/console.js", "algorithm": "sha256-lf", - "sha256": "e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03" + "sha256": "5644af7bb35bb6d0bcfa7eeae9a21b406ac4fc1b499a6c797a6534c7d33f0678" } ] }, diff --git a/scripts/build-console.mjs b/scripts/build-console.mjs new file mode 100644 index 000000000..4fea74887 --- /dev/null +++ b/scripts/build-console.mjs @@ -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.') diff --git a/scripts/site-vendor/README.md b/scripts/site-vendor/README.md index c23b7c43b..5199f2128 100644 --- a/scripts/site-vendor/README.md +++ b/scripts/site-vendor/README.md @@ -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. diff --git a/scripts/site-vendor/console/README.md b/scripts/site-vendor/console/README.md new file mode 100644 index 000000000..6bb3d2f19 --- /dev/null +++ b/scripts/site-vendor/console/README.md @@ -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. diff --git a/scripts/site-vendor/console/build.ts b/scripts/site-vendor/console/build.ts new file mode 100644 index 000000000..dcf4873be --- /dev/null +++ b/scripts/site-vendor/console/build.ts @@ -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() + 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() + 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) +} diff --git a/scripts/site-vendor/console/runtime/entry.ts b/scripts/site-vendor/console/runtime/entry.ts new file mode 100644 index 000000000..908c33fb9 --- /dev/null +++ b/scripts/site-vendor/console/runtime/entry.ts @@ -0,0 +1,10 @@ +import type { DomRuntime, ReactRuntime } from './types.ts' + +export function install(React: ReactRuntime, ReactDOM: DomRuntime, View: ReactRuntime['Component'], host: Record) { + 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 +} diff --git a/scripts/site-vendor/console/runtime/errors.ts b/scripts/site-vendor/console/runtime/errors.ts new file mode 100644 index 000000000..720e28498 --- /dev/null +++ b/scripts/site-vendor/console/runtime/errors.ts @@ -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 + } +} diff --git a/scripts/site-vendor/console/runtime/style.ts b/scripts/site-vendor/console/runtime/style.ts new file mode 100644 index 000000000..2f6bda842 --- /dev/null +++ b/scripts/site-vendor/console/runtime/style.ts @@ -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; + } +` diff --git a/scripts/site-vendor/console/runtime/subscriptions.ts b/scripts/site-vendor/console/runtime/subscriptions.ts new file mode 100644 index 000000000..943029404 --- /dev/null +++ b/scripts/site-vendor/console/runtime/subscriptions.ts @@ -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 + pending: Set + saved: SavedMethod[] + feed: { pointers: Record, 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) + } + } + } +} diff --git a/scripts/site-vendor/console/runtime/types.ts b/scripts/site-vendor/console/runtime/types.ts new file mode 100644 index 000000000..72276c204 --- /dev/null +++ b/scripts/site-vendor/console/runtime/types.ts @@ -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 } +export type ReactRuntime = typeof React +export interface DomRuntime { + render: (element: React.ReactElement, container: Element) => unknown + unmountComponentAtNode: (container: Element) => boolean +} diff --git a/scripts/site-vendor/console/runtime/view.ts b/scripts/site-vendor/console/runtime/view.ts new file mode 100644 index 000000000..2a64919fc --- /dev/null +++ b/scripts/site-vendor/console/runtime/view.ts @@ -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, methods, parse, window) + interface State { logs: Log[], hover: boolean } + return class ConsoleView extends React.Component { + scrollTimer: number | null = null + consoleRef = React.createRef() + 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) + } + } +} diff --git a/scripts/tsconfig.docs.json b/scripts/tsconfig.docs.json index 8e96574ed..491e3b7d9 100644 --- a/scripts/tsconfig.docs.json +++ b/scripts/tsconfig.docs.json @@ -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" ] } diff --git a/test/browser/site-console.spec.js b/test/browser/site-console.spec.js index ee7aab0ab..94821681d 100644 --- a/test/browser/site-console.spec.js +++ b/test/browser/site-console.spec.js @@ -172,3 +172,125 @@ test('frozen historical console error-only rendering loses the message on Firefo expect(result.text).not.toContain(result.message) await testInfo.attach('historical-error-rendering', { contentType: 'application/json', body: JSON.stringify(result) }) }) + +test('candidate console cancels pending scroll and log work on unmount and survives remount', async ({ page }, testInfo) => { + await setup(page, testInfo, false) + expect(await page.evaluate(() => { + window.viewer.add({ method: 'log', data: ['pending-scroll'] }) + window.console.log('queued-before-unmount') + window.ReactDOM.unmountComponentAtNode(document.querySelector('#first')) + const pending = window.scrollTimers.size + window.viewer = window.consoleLog(document.querySelector('#first')) + window.console.log('after-remount') + return pending + })).toBe(0) + await expect(page.locator('.console-header-number')).toHaveText('1') + await expect(page.locator('.console-component')).toHaveText('after-remount') +}) + +for (const removeFirst of [true, false]) { + test(`candidate console retains the surviving viewer when removing ${removeFirst ? 'first' : 'second'}`, async ({ page }, testInfo) => { + await setup(page, testInfo, false) + await page.evaluate(() => { + window.otherViewer = window.consoleLog(document.querySelector('#second')) + window.console.log('both-mounted') + }) + await expect(page.locator('#first .console-header-number')).toHaveText('1') + await expect(page.locator('#second .console-header-number')).toHaveText('1') + await page.evaluate((first) => { + window.ReactDOM.unmountComponentAtNode(document.querySelector(first ? '#first' : '#second')) + window.console.log('survivor-log') + }, removeFirst) + const remaining = removeFirst ? '#second' : '#first' + await expect(page.locator(`${remaining} .console-header-number`)).toHaveText('2') + await expect(page.locator(`${remaining} .console-component`)).toContainText('survivor-log') + expect(await page.evaluate((selector) => { + window.ReactDOM.unmountComponentAtNode(document.querySelector(selector)) + return window.console.log === window.originalConsole.log + }, remaining)).toBe(true) + }) +} + +test('candidate console keeps native Error message and stack visible without modifying Error', async ({ page }, testInfo) => { + await setup(page, testInfo, false) + await page.evaluate(() => { + window.nativeError = new Error('candidate-error-message') + window.originalStack = window.nativeError.stack + window.console.error(window.nativeError) + }) + await expect(page.locator('.console-header-number')).toHaveText('1') + await expect(page.locator('.console-component')).toContainText('candidate-error-message') + expect(await page.evaluate(() => window.nativeError.stack === window.originalStack)).toBe(true) + const stack = await page.evaluate(() => window.originalStack) + for (const line of stack.trim().split('\n')) + await expect(page.locator('.console-component')).toContainText(line.trim()) +}) + +test('candidate console leaves later external wrappers intact and remounts without duplicate forwarding', async ({ page }, testInfo) => { + await setup(page, testInfo, false) + await page.evaluate(() => { + const own = window.console.log + window.externalCalls = 0 + window.external = function (...args) { + window.externalCalls++ + return own.apply(this, args) + } + window.console.log = window.external + window.consoleLog.unmount(document.querySelector('#first')) + window.console.log('no-viewer') + window.viewer = window.consoleLog(document.querySelector('#first')) + window.console.log('remounted') + }) + await expect(page.locator('.console-header-number')).toHaveText('1') + await expect(page.locator('.console-component')).toHaveText('remounted') + expect(await page.evaluate(() => { + window.consoleLog.unmount(document.querySelector('#first')) + return { calls: window.externalCalls, retained: window.console.log === window.external } + })).toEqual({ calls: 2, retained: true }) +}) + +test('editor retains console on persisted pagehide and releases it on final pagehide', async ({ page }) => { + await page.route('https://**/*', route => route.fulfill({ status: 200, body: '' })) + await page.goto('/?code=window.console.log("editor-initial")') + await expect(page.locator('.console-component')).toContainText('editor-initial') + const before = await page.locator('.console-header-number').textContent() + await page.evaluate(() => { + window.dispatchEvent(new PageTransitionEvent('pagehide', { persisted: true })) + window.console.log('persisted-live') + }) + await expect(page.locator('.console-component')).toContainText('persisted-live') + await expect(page.locator('.console-header-number')).toHaveText(String(Number(before) + 1)) + await page.evaluate(() => window.dispatchEvent(new PageTransitionEvent('pagehide', { persisted: false }))) + await expect(page.locator('.console-header')).toHaveCount(0) + expect(await page.evaluate(() => 'feed' in window.console)).toBe(false) +}) + +test('candidate console parses stateful methods once for shared viewers', async ({ page }, testInfo) => { + await setup(page, testInfo, false) + await page.evaluate(() => { + window.otherViewer = window.consoleLog(document.querySelector('#second')) + window.console.count('shared-count') + window.console.count('shared-count') + window.console.assert(true, 'must-not-log') + window.console.assert(false, 'visible-assertion') + window.console.time('shared-time') + window.console.timeEnd('shared-time') + }) + await expect(page.locator('#first .console-header-number')).toHaveText('4') + await expect(page.locator('#second .console-header-number')).toHaveText('4') + const result = await page.evaluate(() => ({ + a: window.viewer.state.logs, + b: window.otherViewer.state.logs, + sameRecords: window.viewer.state.logs.every((row, index) => row === window.otherViewer.state.logs[index]), + })) + expect(result.a).toEqual(result.b) + expect(result.sameRecords).toBe(true) + expect(JSON.stringify(result.a)).toContain('shared-count: 1') + expect(JSON.stringify(result.a)).toContain('shared-count: 2') + await expect(page.locator('#first .console-component')).toContainText('visible-assertion') + await expect(page.locator('#first .console-component')).not.toContainText('must-not-log') + await page.evaluate(() => window.console.clear()) + await expect(page.locator('#first .console-header-number')).toHaveText('5') + await expect(page.locator('#second .console-header-number')).toHaveText('5') + expect(await page.evaluate(() => window.viewer.state.logs.at(-1).method)).toBe('clear') +}) diff --git a/test/site-console.test.js b/test/site-console.test.js new file mode 100644 index 000000000..396a844bf --- /dev/null +++ b/test/site-console.test.js @@ -0,0 +1,177 @@ +import assert from 'node:assert/strict' +import { createHash } from 'node:crypto' +import fs from 'node:fs' +import process from 'node:process' +// eslint-disable-next-line test/no-import-node-test -- Native console subscription and frozen build boundaries. +import test from 'node:test' +import ts from 'typescript' +import { generateConsole, moduleRanges, obsoleteMap, upstreamSha256 } from '../scripts/site-vendor/console/build.ts' +import { errorArgument } from '../scripts/site-vendor/console/runtime/errors.ts' +import { css } from '../scripts/site-vendor/console/runtime/style.ts' +import { createSubscriptions } from '../scripts/site-vendor/console/runtime/subscriptions.ts' + +function fixture() { + const jobs = new Map() + let id = 0 + const calls = [] + const parsed = [] + const clock = { + setTimeout(fn) { + jobs.set(++id, fn) + return id + }, + clearTimeout(key) { jobs.delete(key) }, + } + const original = function (...args) { + calls.push({ receiver: this, args }) + return 123 + } + const target = { log: original } + const subscribe = createSubscriptions(target, ['log'], (method, data) => { + parsed.push({ method, data }) + return { method, data } + }, clock) + const flush = () => { + for (const [key, fn] of [...jobs]) { + jobs.delete(key) + fn() + } + } + return { jobs, calls, parsed, target, original, subscribe, flush, clock } +} + +test('Console subscriptions forward once with the original receiver and independently remove either viewer', () => { + for (const first of [true, false]) { + const f = fixture() + const a = [] + const b = [] + const offA = f.subscribe(log => a.push(log)) + const hook = f.target.log + const offB = f.subscribe(log => b.push(log)) + assert.equal(f.target.log, hook) + const receiver = {} + const object = { nested: true } + assert.equal(f.target.log.call(receiver, object), undefined) + assert.equal(f.calls.length, 1) + assert.equal(f.calls[0].receiver, receiver) + f.flush() + assert.equal(f.parsed.length, 1) + assert.equal(a[0].data[0], object) + assert.equal(b[0].data[0], object) + ;(first ? offA : offB)() + f.target.log('survivor') + f.flush() + assert.equal(first ? b.length : a.length, 2) + ;(first ? offB : offA)() + assert.equal(f.target.log, f.original) + assert(!('feed' in f.target)) + } +}) + +test('Console subscriptions cancel abandoned logs and do not replay prior logs into a new viewer', () => { + const f = fixture() + const a = [] + const b = [] + const offA = f.subscribe(log => a.push(log)) + f.target.log('old') + const offB = f.subscribe(log => b.push(log)) + offA() + f.flush() + assert.deepEqual([a, b], [[], []]) + f.target.log('cancelled') + offB() + assert.equal(f.jobs.size, 0) + const off = f.subscribe(log => a.push(log)) + f.target.log('new') + f.flush() + assert.deepEqual(a.map(log => log.data[0]), ['new']) + off() +}) + +test('Console ownership preserves preexisting metadata and wrappers installed later', () => { + const f = fixture() + const previous = { pointers: { unrelated: true } } + const rows = [] + Object.defineProperty(f.target, 'feed', { configurable: true, value: previous, writable: true, enumerable: false }) + const before = Object.getOwnPropertyDescriptor(f.target, 'feed') + const off = f.subscribe(log => rows.push(log)) + const own = f.target.log + const foreign = function (...args) { + return own.apply(this, args) + } + f.target.log = foreign + off() + assert.equal(f.target.log, foreign) + assert.deepEqual(Object.getOwnPropertyDescriptor(f.target, 'feed'), before) + f.target.log('after-dispose') + assert.equal(f.calls.length, 1) + assert.equal(f.jobs.size, 0) + assert.deepEqual(rows, []) + const again = f.subscribe(log => rows.push(log)) + f.target.log('again') + f.flush() + assert.equal(rows.length, 1) + again() + assert.equal(f.target.log, foreign) +}) + +test('Console installation rolls back on a readonly method and keeps other viewers live after a callback throws', () => { + const f = fixture() + Object.defineProperty(f.target, 'error', { value() {}, configurable: true }) + const subscribe = createSubscriptions(f.target, ['log', 'error'], () => false, f.clock) + assert.throws(() => subscribe(() => {}), TypeError) + assert.equal(f.target.log, f.original) + assert(!('feed' in f.target)) + const failure = new Error('listener failed') + const rows = [] + const offA = f.subscribe(() => { + throw failure + }) + const offB = f.subscribe(log => rows.push(log)) + f.target.log('delivery') + assert.throws(f.flush, error => error === failure) + assert.equal(rows.length, 1) + offA() + offB() +}) + +test('Console error adaptation keeps existing stacks and arbitrary objects, and adds missing native messages without mutation', () => { + const error = new Error('message') + const object = { stack: 'custom', message: 'keep-object' } + assert.equal(errorArgument(object), object) + assert.equal(errorArgument(error), error) + error.stack = 'method@file:1:2' + assert.equal(errorArgument(error), 'Error: message\nmethod@file:1:2') + assert.equal(error.stack, 'method@file:1:2') + error.message = 'method' + assert.equal(errorArgument(error), 'Error: method\nmethod@file:1:2') + Object.defineProperty(error, 'message', { + get() { throw new Error('getter') }, + }) + assert.equal(errorArgument(error), error) +}) + +test('Console build changes only the two owned module bodies and is deterministic', async () => { + const source = fs.readFileSync('refactor/baselines/site-vendor/console-original.js', 'utf8') + assert.equal(createHash('sha256').update(source).digest('hex'), upstreamSha256) + const mask = (text) => { + for (const [id, range] of [...moduleRanges(text)].sort((a, b) => b[1].start - a[1].start)) + text = text.slice(0, range.start) + id + text.slice(range.end) + return text + } + const candidate = await generateConsole(process.cwd()) + const range = moduleRanges(source).get('W5CS') + const view = ts.createSourceFile('view.js', source.slice(range.start, range.end), ts.ScriptTarget.Latest, true) + const styles = [] + function visit(node) { + if (ts.isStringLiteral(node) && node.text.includes('position: relative;')) + styles.push(node.text) + ts.forEachChild(node, visit) + } + visit(view) + assert.deepEqual(styles, [css]) + assert.equal(mask(candidate), mask(source).slice(0, -obsoleteMap.length)) + assert.equal(candidate, await generateConsole(process.cwd())) + assert.notEqual(candidate, source) + assert.throws(() => moduleRanges('var unrelated = 1'), /Missing owned/) +})