refactor(site): [SITE-CONSOLE-01] restore typed console and lifecycle ownership

This commit is contained in:
Harvey Zhao committed 2026-09-15 06:15:32 +08:00
1 parent 6cd2d0ffbe
commit 3dba82a453
29 files changed
+1884 -36

No files matched your search

+3
View File
@@ -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));
+40 -3
View File
@@ -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;t<arguments.length;t++){var r=arguments[t];for(var n in r)Object.prototype.hasOwnProperty.call(r,n)&&(e[n]=r[n])}return e}).apply(this,arguments)}var d=function(e,t){for(var r=[e[0]],n=0,o=t.length;n<o;n+=1)r.push(t[n],e[n+1]);return r},p=function(e){return null!==e&&"object"==f(e)&&"[object Object]"===(e.toString?e.toString():Object.prototype.toString.call(e))&&!(0,t.typeOf)(e)},v=Object.freeze([]),g=Object.freeze({});function m(e){return"function"==typeof e}function y(e){return e.displayName||e.name||"Component"}function S(e){return e&&"string"==typeof e.styledComponentId}var b="data-styled",w="5.3.3",x="undefined"!=typeof window&&"HTMLElement"in window,C=Boolean("boolean"==typeof SC_DISABLE_SPEEDY&&SC_DISABLE_SPEEDY),I={},A={};function P(){for(var e=arguments.length<=0?void 0:arguments[0],t=[],r=1,n=arguments.length;r<n;r+=1)t.push(r<0||arguments.length<=r?void 0:arguments[r]);return t.forEach(function(t){e=e.replace(/%[a-z]/,t)}),e}function j(e){for(var t=arguments.length,r=new Array(t>1?t-1:0),n=1;n<t;n++)r[n-1]=arguments[n];throw new Error("An error occurred. See https://git.io/JUIaE#"+e+" for more information."+(r.length>0?" 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<e;r++)t+=this.groupSizes[r];return t},t.insertRules=function(e,t){if(e>=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<o;i++)this.groupSizes[i]=0}for(var s=this.indexOfGroup(e+1),a=0,u=t.length;a<u;a++)this.tag.insertRule(s,t[a])&&(this.groupSizes[e]++,s++)},t.clearGroup=function(e){if(e<this.length){var t=this.groupSizes[e],r=this.indexOfGroup(e),n=r+t;this.groupSizes[e]=0;for(var o=r;o<n;o++)this.tag.deleteRule(r)}},t.getGroup=function(e){var t="";if(e>=this.length||0===this.groupSizes[e])return t;for(var r=this.groupSizes[e],n=this.indexOfGroup(e),o=n+r,i=n;i<o;i++)t+=this.tag.getRule(i)+"/*!sc*/\n";return t},e}(),_=new Map,O=new Map,N=1,E=function(e){if(_.has(e))return _.get(e);for(;O.has(N);)N++;var t=N++;return _.set(e,t),O.set(t,e),t},T=function(e){return O.get(e)},k=function(e,t){t>=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<s;i++)(n=o[i])&&e.registerName(t,n)},q=function(e,t){for(var r=(t.textContent||"").split("/*!sc*/\n"),n=[],o=0,i=r.length;o<i;o++){var s=r[o].trim();if(s){var a=s.match(z);if(a){var u=0|parseInt(a[1],10),l=a[2];0!==u&&(k(l,u),F(e,l,a[3]),e.getTag().insertRules(u,n)),n.length=0}else n.push(s)}}},G=function(){return"undefined"!=typeof window&&void 0!==window.__webpack_nonce__?window.__webpack_nonce__:null},B=function(e){var t=document.head,r=e||t,n=document.createElement("style"),o=function(e){for(var t=e.childNodes,r=t.length;r>=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=functionLine truncated
},{"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<t;n++)o[n]=e[n];return o}function d(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function p(e,t){for(var n=0;n<t.length;n++){var o=t[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(e,o.key,o)}}function y(e,t,n){return t&&p(e.prototype,t),n&&p(e,n),e}function h(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),t&&m(e,t)}function m(e,t){return(m=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e})(e,t)}function v(e){var t=w();return function(){var n,o=E(e);if(t){var r=E(this).constructor;n=Reflect.construct(o,arguments,r)}else n=o.apply(this,arguments);return b(this,n)}}function b(e,t){if(t&&("object"===l(t)||"function"==typeof t))return t;if(void 0!==t)throw new TypeError("Derived constructors may only return object or undefined");return g(e)}function g(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}function w(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){})),!0}catch(e){return!1}}function E(e){return(E=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)})(e)}function O(e,t){return t||(t=e.slice(0)),Object.freeze(Object.defineProperties(e,{raw:{value:Object.freeze(t)}}))}var j=o.default.div(e||(e=O(["\n position: relative;\n height: 100%;\n display: flex;\n flex-direction: column;\n background-color: #242424;\n .console-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n color: #eee;\n padding: 7px 10px;\n font-size: 12px;\n background-color: #111;\n border-bottom: 1px solid rgb(255 255 255 / 10%);\n user-select: none;\n .console-header-left {\n display: flex;\n align-items: center;\n .console-header-number {\n margin-left: 5px;\n color: rgba(255, 255, 255, 0.4);\n }\n }\n .console-header-right {\n cursor: pointer;\n }\n }\n .console-component {\n scroll-behavior: smooth;\n position: relative;\n overflow: auto;\n height: 200px;\n flex: 1;\n }\n"]))),x=function(e){h(r,t.default.Component);var o=v(r);function r(e){var n;return d(this,r),(n=o.call(this,e)).scrollTimer=null,n.consoleRef=t.default.createRef(),n.onClear=n.onClear.bind(g(n)),n.onMouseEnter=n.onMouseEnter.bind(g(n)),n.onMouseLeave=n.onMouseLeave.bind(g(n)),n.state={logs:[],hover:!1},n}return y(r,[{key:"componentDidMount",value:function(){var e=this;(0,n.Hook)(window.console,function(t){return e.add(t)},!1)}},{key:"add",value:function(e){this.setState({logs:[].concat(i(this.state.logs),[e])});var t=this.consoleRef.current;!this.state.hover&&t&&(clearTimeout(this.scrollTimer),this.scrollTimer=setTimeout(function(){t.scrollTop=t.scrollHeight},200))}},{key:"onClear",value:function(){this.setState({logs:[]})}},{key:"onMouseEnter",value:function(){this.setState({hover:!0})}},{key:"onMouseLeave",value:function(){this.setState({hover:!1})}},{key:"componentWillUnmount",value:function(){(0,n.Unhook)(window.console)}},{key:"render",value:function(){return t.default.createElement(j,null,t.default.createElement("div",{className:"console-header"},t.default.createElement("div",{className:"console-header-left"},"Console"," ",t.default.createElement("span",{className:"console-header-number"},this.state.logs.length)),t.default.createElement("div",{className:"console-header-right",onClick:this.onClear},"Clear")),t.default.createLine truncated
"use strict";"use strict";var __consoleOwned=(()=>{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
+7 -4
View File
@@ -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": {
+5 -2
View File
@@ -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.
@@ -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.
@@ -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<typeof createLibraryLoader> & {
exampleSource: typeof exampleSource
parameters: typeof parameters
@@ -25,6 +25,9 @@ export async function installEditor(host: EditorHost): Promise<void> {
host.consoleLog(output)
const controller = new AbortController()
const cleanups: (() => void)[] = []
cleanups.push(() => {
host.consoleLog.unmount?.(output)
})
const listen = (
target: EventTarget,
type: string,
File diff suppressed because it is too large. Load diff
@@ -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 仍需完整站点验收。无推送或发布。
+26 -3
View File
@@ -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 必须新增候选“无泄漏/不丢日志/错误消息可见”
+3 -3
View File
@@ -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<br>验证 Vue 消费者与更新卸载 | CORE-22, ENG-07 | Vue 实例/ref、参数更新、卸载及插件样例 | 旧 JS 组件用法无需修改,重复挂载不泄漏 | M | done |
| EX-03 | workspace<br>验证全部原生 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<br>修复 vConsole 日志帧与延迟面板销毁竞态 | SITE-01 | 冻结上游 bundle 的可复现 TS 生命周期补丁、旧红新绿回归、来源及生成检查;销毁后虚拟列表布局续段停止 | 最后日志插件移除后取消 RAF 并隔离旧队列;延迟面板不得写入已销毁或替换插件;三引擎移动页与 UMD 入口通过,维护声明与原有许可缺口分别记录;原生 ResizeObserver 布局与挂起 timer 旧红新绿 | M | done |
| SITE-CONSOLE-01 | artplayer-vitepress, workspace<br>迁移控制台自有 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<br>迁移控制台自有 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)
+8
View File
@@ -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项共同
+3 -3
View File
@@ -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 |
+33 -9
View File
@@ -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."
}
]
}
+3 -2
View File
@@ -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"
]
}
]
+2 -2
View File
@@ -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"
}
]
},
+12
View File
@@ -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.')
+6 -2
View File
@@ -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.
+69
View File
@@ -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.
+48
View File
@@ -0,0 +1,48 @@
import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import fs from 'node:fs'
import path from 'node:path'
import { build } from 'esbuild'
import ts from 'typescript'
export const upstreamSha256 = 'e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03'
export const obsoleteMap = '//# sourceMappingURL=/index.js.map'
export function moduleRanges(code: string) {
const source = ts.createSourceFile('console.js', code, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS)
const ranges = new Map<string, { start: number, end: number }>()
function visit(node: ts.Node) {
if (ts.isPropertyAssignment(node) && ts.isStringLiteral(node.name) && ['W5CS', 'Focm'].includes(node.name.text)) {
assert(ts.isArrayLiteralExpression(node.initializer))
const fn = node.initializer.elements[0]
assert(fn && ts.isFunctionExpression(fn))
assert(!ranges.has(node.name.text), 'Duplicate owned console module')
ranges.set(node.name.text, { start: fn.body.getStart(source) + 1, end: fn.body.end - 1 })
}
ts.forEachChild(node, visit)
}
visit(source)
assert.equal(ranges.size, 2, 'Missing owned console modules')
return ranges
}
export async function generateConsole(root: string) {
const upstream = fs.readFileSync(path.join(root, 'refactor/baselines/site-vendor/console-original.js'), 'utf8')
assert.equal(createHash('sha256').update(upstream).digest('hex'), upstreamSha256, 'Frozen console source changed')
const ranges = moduleRanges(upstream)
const bodies = new Map<string, string>()
for (const [id, entry] of [['W5CS', 'view'], ['Focm', 'entry']] as const) {
const result = await build({ entryPoints: [path.join(root, `scripts/site-vendor/console/runtime/${entry}.ts`)], bundle: true, write: false, platform: 'browser', format: 'iife', globalName: '__consoleOwned', target: 'es2015', minify: true, legalComments: 'none', logLevel: 'silent' })
const compiled = result.outputFiles[0]?.text
assert(compiled)
const expression = id === 'W5CS'
? '__consoleOwned.createView(require("react"),require("console-feed"),require("styled-components").default,require("RxPG").default,require("V2JG").default)'
: '__consoleOwned.install(require("react"),require("react-dom"),require("./Console").default,window)'
bodies.set(id, `\n"use strict";${compiled}Object.defineProperty(exports,"__esModule",{value:true});exports.default=${expression};\n`)
}
let output = upstream
for (const [id, range] of [...ranges].sort((a, b) => b[1].start - a[1].start))
output = output.slice(0, range.start) + bodies.get(id) + output.slice(range.end)
assert(output.endsWith(obsoleteMap), 'Unexpected original console source map trailer')
return output.slice(0, -obsoleteMap.length)
}
@@ -0,0 +1,10 @@
import type { DomRuntime, ReactRuntime } from './types.ts'
export function install(React: ReactRuntime, ReactDOM: DomRuntime, View: ReactRuntime['Component'], host: Record<string, unknown>) {
const consoleLog = (element: Element) => ReactDOM.render(React.createElement(View), element)
consoleLog.unmount = (element: Element) => ReactDOM.unmountComponentAtNode(element)
host.React = React
host.ReactDOM = ReactDOM
host.consoleLog = consoleLog
return consoleLog
}
@@ -0,0 +1,17 @@
export function errorArgument(value: unknown): unknown {
try {
if (Object.prototype.toString.call(value) !== '[object Error]')
return value
const error = value as Error
const stack = error.stack
const message = error.message
if (typeof stack !== 'string' || !message)
return value
const header = error.name ? `${error.name}: ${message}` : message
return stack.startsWith(header) ? value : `${header}\n${stack}`
}
catch {
// Keep the legacy parser's behavior for unusual throwing Error getters.
return value
}
}
@@ -0,0 +1,36 @@
export const css = `
position: relative;
height: 100%;
display: flex;
flex-direction: column;
background-color: #242424;
.console-header {
display: flex;
justify-content: space-between;
align-items: center;
color: #eee;
padding: 7px 10px;
font-size: 12px;
background-color: #111;
border-bottom: 1px solid rgb(255 255 255 / 10%);
user-select: none;
.console-header-left {
display: flex;
align-items: center;
.console-header-number {
margin-left: 5px;
color: rgba(255, 255, 255, 0.4);
}
}
.console-header-right {
cursor: pointer;
}
}
.console-component {
scroll-behavior: smooth;
position: relative;
overflow: auto;
height: 200px;
flex: 1;
}
`
@@ -0,0 +1,107 @@
import type { Clock, ConsoleTarget, Log, NativeMethod, Parser } from './types.ts'
import { errorArgument } from './errors.ts'
interface Listener { callback?: (log: Log) => void }
interface SavedMethod { name: string, descriptor?: PropertyDescriptor, wrapper: NativeMethod }
interface Owner {
listeners: Set<Listener>
pending: Set<number>
saved: SavedMethod[]
feed: { pointers: Record<string, unknown>, src: { npm: string, github: string } }
previousFeed?: PropertyDescriptor
}
export function createSubscriptions(target: ConsoleTarget, methods: string[], parse: Parser, clock: Clock) {
let owner: Owner | undefined
function restore(current: Owner) {
for (const timer of current.pending)
clock.clearTimeout(timer)
current.pending.clear()
for (const saved of current.saved) {
if (target[saved.name] !== saved.wrapper)
continue
if (saved.descriptor)
Object.defineProperty(target, saved.name, saved.descriptor)
else
delete target[saved.name]
}
if (target.feed === current.feed) {
if (current.previousFeed)
Object.defineProperty(target, 'feed', current.previousFeed)
else
delete target.feed
}
}
return (callback: (log: Log) => void) => {
if (!owner) {
const current: Owner = {
listeners: new Set(),
pending: new Set(),
saved: [],
previousFeed: Object.getOwnPropertyDescriptor(target, 'feed'),
feed: { pointers: {}, src: { npm: 'https://npmjs.com/package/console-feed', github: 'https://github.com/samdenty99/console-feed' } },
}
owner = current
try {
for (const name of methods) {
const original = target[name] as NativeMethod
const wrapper: NativeMethod = function (...args) {
Reflect.apply(original, this, args)
const listeners = [...current.listeners]
if (!listeners.length)
return
const timer = clock.setTimeout(() => {
current.pending.delete(timer)
const log = parse(name, name === 'error' ? args.map(errorArgument) : args)
if (log) {
let failed = false
let failure: unknown
for (const listener of listeners) {
try {
listener.callback?.(log)
}
catch (error) {
if (!failed) {
failed = true
failure = error
}
else {
clock.setTimeout(() => {
throw error
}, 0)
}
}
}
if (failed)
throw failure
}
}, 0)
current.pending.add(timer)
}
current.feed.pointers[name] = original
current.saved.push({ name, descriptor: Object.getOwnPropertyDescriptor(target, name), wrapper })
target[name] = wrapper
}
target.feed = current.feed
}
catch (error) {
restore(current)
owner = undefined
throw error
}
}
const current = owner
const listener: Listener = { callback }
current.listeners.add(listener)
return () => {
listener.callback = undefined
current.listeners.delete(listener)
if (!current.listeners.size && owner === current) {
owner = undefined
restore(current)
}
}
}
}
@@ -0,0 +1,17 @@
import type * as React from 'react'
export interface Log { method: string, id?: string, data?: unknown[] }
export type Parser = (method: string, args: unknown[], id?: string) => Log | false
export type NativeMethod = (this: unknown, ...args: unknown[]) => unknown
export interface ConsoleTarget { [method: string]: unknown }
export interface Clock {
setTimeout: (callback: () => void, delay: number) => number
clearTimeout: (id: number) => void
}
export interface Feed { Console: React.ComponentType<{ logs: Log[], variant: string }> }
export interface Styled { div: (strings: TemplateStringsArray) => React.ComponentType<React.PropsWithChildren> }
export type ReactRuntime = typeof React
export interface DomRuntime {
render: (element: React.ReactElement, container: Element) => unknown
unmountComponentAtNode: (container: Element) => boolean
}
@@ -0,0 +1,68 @@
import type { Feed, Log, Parser, ReactRuntime, Styled } from './types.ts'
import { css } from './style.ts'
import { createSubscriptions } from './subscriptions.ts'
export function createView(React: ReactRuntime, feed: Feed, styled: Styled, parse: Parser, methods: string[]) {
const template = Object.freeze(Object.defineProperty([css], 'raw', { value: Object.freeze([css]) })) as unknown as TemplateStringsArray
const Container = styled.div(template)
const subscribe = createSubscriptions(window.console as unknown as Record<string, unknown>, methods, parse, window)
interface State { logs: Log[], hover: boolean }
return class ConsoleView extends React.Component<object, State> {
scrollTimer: number | null = null
consoleRef = React.createRef<HTMLDivElement>()
private unsubscribe?: () => void
private mounted = false
constructor(props: object) {
super(props)
this.onClear = this.onClear.bind(this)
this.onMouseEnter = this.onMouseEnter.bind(this)
this.onMouseLeave = this.onMouseLeave.bind(this)
this.state = { logs: [], hover: false }
}
override componentDidMount() {
this.mounted = true
this.unsubscribe = subscribe(log => this.add(log))
}
add(log: Log) {
if (!this.mounted)
return
this.setState(state => ({ logs: [...state.logs, log] }))
const element = this.consoleRef.current
if (!this.state.hover && element) {
if (this.scrollTimer !== null)
window.clearTimeout(this.scrollTimer)
this.scrollTimer = window.setTimeout(() => {
this.scrollTimer = null
if (this.mounted && this.consoleRef.current === element)
element.scrollTop = element.scrollHeight
}, 200)
}
}
onClear() { this.setState({ logs: [] }) }
onMouseEnter() { this.setState({ hover: true }) }
onMouseLeave() { this.setState({ hover: false }) }
override componentWillUnmount() {
this.mounted = false
this.unsubscribe?.()
this.unsubscribe = undefined
if (this.scrollTimer !== null)
window.clearTimeout(this.scrollTimer)
this.scrollTimer = null
}
override render() {
const count = React.createElement('span', { className: 'console-header-number' }, this.state.logs.length)
const title = React.createElement('div', { className: 'console-header-left' }, 'Console', ' ', count)
const clear = React.createElement('div', { className: 'console-header-right', onClick: this.onClear }, 'Clear')
const header = React.createElement('div', { className: 'console-header' }, title, clear)
const logs = React.createElement(feed.Console, { logs: this.state.logs, variant: 'dark' })
const content = React.createElement('div', { ref: this.consoleRef, className: 'console-component', onMouseEnter: this.onMouseEnter, onMouseLeave: this.onMouseLeave }, logs)
return React.createElement(Container, null, header, content)
}
}
}
+2 -1
View File
@@ -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"
]
}
+122
View File
@@ -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')
})
+177
View File
@@ -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/)
})