mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
refactor(site): [SITE-CONSOLE-01] restore typed console and lifecycle ownership
This commit is contained in:
1 parent
6cd2d0ffbe
commit
3dba82a453
29 files changed
+1884
-36
No files matched your search
@@ -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));
|
||||
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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 仍需完整站点验收。无推送或发布。
|
||||
@@ -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
@@ -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)
|
||||
@@ -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项共同
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import process from 'node:process'
|
||||
import { generateConsole } from './site-vendor/console/build.ts'
|
||||
|
||||
assert(process.argv.slice(2).every(arg => arg === '--check'), 'Use yarn build:console [--check]')
|
||||
const candidate = await generateConsole(process.cwd())
|
||||
const output = 'docs/assets/js/console.js'
|
||||
if (process.argv.includes('--check'))
|
||||
assert.equal(fs.readFileSync(output, 'utf8'), candidate, 'Generated console drift')
|
||||
else fs.writeFileSync(output, candidate)
|
||||
console.log('Console TS entry and view generated/checked; 100 frozen vendor modules and Parcel runtime retained.')
|
||||
@@ -55,6 +55,10 @@ members as Buffers. Compare `package/dist/codicon.ttf` directly with `fontPath`,
|
||||
and each non-null notice member with its frozen `source`. Do not pipe binary font
|
||||
output through PowerShell text redirection. No dependency installation is needed.
|
||||
|
||||
Follow-up: finish Monaco's broader bundled-component notice audit, then recover the
|
||||
console.js build and resolve remaining fonts/media. Do not upgrade these assets
|
||||
The desktop console's owned TS entry/view and lifecycle now build through
|
||||
`build:console` / `check:console`; see [console maintenance](console/README.md).
|
||||
Its other 100 Parcel modules remain frozen pending full provenance and notices.
|
||||
|
||||
Follow-up: finish Monaco's broader bundled-component notice audit, the console
|
||||
vendor provenance and remaining fonts/media. Do not upgrade these assets
|
||||
without verifying globals, AMD/worker paths, CSS, consoleLog and user interaction.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Desktop console maintenance
|
||||
|
||||
`yarn build:console` generates `docs/assets/js/console.js`; `yarn check:console`
|
||||
compares it without writing. Never edit that output. This is the desktop editor
|
||||
console, separate from the mobile vConsole integration.
|
||||
|
||||
## Modules and ownership
|
||||
|
||||
| File | Responsibility |
|
||||
| -------------------------- | ------------------------------------------------------------------------------------ |
|
||||
| `build.ts` | Verify frozen input, compile two owned modules, replace their Parcel function bodies |
|
||||
| `runtime/entry.ts` | Install React, ReactDOM and consoleLog globals; return the React render result |
|
||||
| `runtime/view.ts` | Component state, Clear, hover, scroll scheduling and unmount |
|
||||
| `runtime/subscriptions.ts` | One hook owner, subscriptions, queued delivery and descriptor restoration |
|
||||
| `runtime/errors.ts` | Preserve native error message and stack without mutating the input |
|
||||
| `runtime/style.ts` | Original stylesheet, decoded exactly from the frozen view |
|
||||
| `runtime/types.ts` | Structural interfaces for injected runtimes and parser |
|
||||
|
||||
The fixed upstream is `refactor/baselines/site-vendor/console-original.js`, checked
|
||||
by SHA-256. Only W5CS (view) and Focm (entry) bodies change. The other 100 modules,
|
||||
their dependency maps and Parcel runtime remain byte-for-byte intact. The dead
|
||||
`/index.js.map` trailer is removed; a complete upstream source map is unavailable.
|
||||
The entry injects existing React/ReactDOM 17.0.2, console-feed and
|
||||
styled-components 5.3.3. RxPG supplies the historical parser and V2JG the method
|
||||
list. Root `@types/react` 19.1.10 is used for type checking only; no React 19 runtime
|
||||
is included. These module IDs are a frozen build boundary, not a general Parcel
|
||||
patching API. Changing the vendor requires a separate provenance review.
|
||||
|
||||
## Data and cleanup
|
||||
|
||||
Each native call forwards once with its receiver, then queues parsing. Wrappers
|
||||
retain the old undefined return behavior. Count/timing/assert parsing runs once
|
||||
per call, and active viewers receive the same parsed record; raw object references
|
||||
remain available to the old inspector. Error-only logs add a missing error header
|
||||
before the historical stack conversion, preserving the original Error object.
|
||||
|
||||
The first subscriber captures method/feed descriptors and installs the hook.
|
||||
Additional viewers share it. Delivery snapshots subscribers when the call occurs;
|
||||
removed viewers do not receive it and later viewers do not receive old logs.
|
||||
The final subscriber cancels pending delivery and restores only wrappers and
|
||||
metadata still owned by this instance. Later external wrappers remain intact.
|
||||
An external wrapper retaining an old hook can forward native calls without
|
||||
retaining viewer callbacks. Failed installation rolls back. A throwing listener
|
||||
does not starve other viewers; its error remains observable asynchronously.
|
||||
No general interception of third-party console mutations is attempted.
|
||||
|
||||
The component owns its 200ms scroll timer and cancels it on unmount; the callback
|
||||
also verifies the live element. `consoleLog(element)` still returns the React
|
||||
component, and repeat mounting into the same container reuses it. The additive
|
||||
`consoleLog.unmount(element)` forwards ReactDOM's boolean result. The editor uses
|
||||
it on non-persisted pagehide. Persisted pagehide retains state; controlled lifecycle
|
||||
tests do not certify physical-device BFCache behavior.
|
||||
|
||||
```sh
|
||||
yarn build:console
|
||||
yarn check:console
|
||||
yarn typecheck:docs-tools
|
||||
yarn test:site-console
|
||||
yarn build:site-assets
|
||||
yarn test:browser test/browser/site-console.spec.js test/browser/site-editor.spec.js --workers=1
|
||||
```
|
||||
|
||||
Historical browser cases intentionally reproduce frozen defects; candidate cases
|
||||
prove their repairs independently. Unit tests check shared ownership, failed
|
||||
installation, callback errors, object identity, exact CSS and vendor preservation.
|
||||
No new dependency is required. Existing esbuild 0.27.7 and TypeScript 5.9.3 provide
|
||||
generation and structural parsing. Full console dependency provenance and notices
|
||||
remain SITE-07 / VENDOR-08; this build does not close them. See
|
||||
`refactor/console-modernization.md` for contracts and evidence.
|
||||
@@ -0,0 +1,48 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { createHash } from 'node:crypto'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { build } from 'esbuild'
|
||||
import ts from 'typescript'
|
||||
|
||||
export const upstreamSha256 = 'e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03'
|
||||
export const obsoleteMap = '//# sourceMappingURL=/index.js.map'
|
||||
|
||||
export function moduleRanges(code: string) {
|
||||
const source = ts.createSourceFile('console.js', code, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS)
|
||||
const ranges = new Map<string, { start: number, end: number }>()
|
||||
function visit(node: ts.Node) {
|
||||
if (ts.isPropertyAssignment(node) && ts.isStringLiteral(node.name) && ['W5CS', 'Focm'].includes(node.name.text)) {
|
||||
assert(ts.isArrayLiteralExpression(node.initializer))
|
||||
const fn = node.initializer.elements[0]
|
||||
assert(fn && ts.isFunctionExpression(fn))
|
||||
assert(!ranges.has(node.name.text), 'Duplicate owned console module')
|
||||
ranges.set(node.name.text, { start: fn.body.getStart(source) + 1, end: fn.body.end - 1 })
|
||||
}
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
visit(source)
|
||||
assert.equal(ranges.size, 2, 'Missing owned console modules')
|
||||
return ranges
|
||||
}
|
||||
|
||||
export async function generateConsole(root: string) {
|
||||
const upstream = fs.readFileSync(path.join(root, 'refactor/baselines/site-vendor/console-original.js'), 'utf8')
|
||||
assert.equal(createHash('sha256').update(upstream).digest('hex'), upstreamSha256, 'Frozen console source changed')
|
||||
const ranges = moduleRanges(upstream)
|
||||
const bodies = new Map<string, string>()
|
||||
for (const [id, entry] of [['W5CS', 'view'], ['Focm', 'entry']] as const) {
|
||||
const result = await build({ entryPoints: [path.join(root, `scripts/site-vendor/console/runtime/${entry}.ts`)], bundle: true, write: false, platform: 'browser', format: 'iife', globalName: '__consoleOwned', target: 'es2015', minify: true, legalComments: 'none', logLevel: 'silent' })
|
||||
const compiled = result.outputFiles[0]?.text
|
||||
assert(compiled)
|
||||
const expression = id === 'W5CS'
|
||||
? '__consoleOwned.createView(require("react"),require("console-feed"),require("styled-components").default,require("RxPG").default,require("V2JG").default)'
|
||||
: '__consoleOwned.install(require("react"),require("react-dom"),require("./Console").default,window)'
|
||||
bodies.set(id, `\n"use strict";${compiled}Object.defineProperty(exports,"__esModule",{value:true});exports.default=${expression};\n`)
|
||||
}
|
||||
let output = upstream
|
||||
for (const [id, range] of [...ranges].sort((a, b) => b[1].start - a[1].start))
|
||||
output = output.slice(0, range.start) + bodies.get(id) + output.slice(range.end)
|
||||
assert(output.endsWith(obsoleteMap), 'Unexpected original console source map trailer')
|
||||
return output.slice(0, -obsoleteMap.length)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { DomRuntime, ReactRuntime } from './types.ts'
|
||||
|
||||
export function install(React: ReactRuntime, ReactDOM: DomRuntime, View: ReactRuntime['Component'], host: Record<string, unknown>) {
|
||||
const consoleLog = (element: Element) => ReactDOM.render(React.createElement(View), element)
|
||||
consoleLog.unmount = (element: Element) => ReactDOM.unmountComponentAtNode(element)
|
||||
host.React = React
|
||||
host.ReactDOM = ReactDOM
|
||||
host.consoleLog = consoleLog
|
||||
return consoleLog
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export function errorArgument(value: unknown): unknown {
|
||||
try {
|
||||
if (Object.prototype.toString.call(value) !== '[object Error]')
|
||||
return value
|
||||
const error = value as Error
|
||||
const stack = error.stack
|
||||
const message = error.message
|
||||
if (typeof stack !== 'string' || !message)
|
||||
return value
|
||||
const header = error.name ? `${error.name}: ${message}` : message
|
||||
return stack.startsWith(header) ? value : `${header}\n${stack}`
|
||||
}
|
||||
catch {
|
||||
// Keep the legacy parser's behavior for unusual throwing Error getters.
|
||||
return value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
export const css = `
|
||||
position: relative;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #242424;
|
||||
.console-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: #eee;
|
||||
padding: 7px 10px;
|
||||
font-size: 12px;
|
||||
background-color: #111;
|
||||
border-bottom: 1px solid rgb(255 255 255 / 10%);
|
||||
user-select: none;
|
||||
.console-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.console-header-number {
|
||||
margin-left: 5px;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
}
|
||||
.console-header-right {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.console-component {
|
||||
scroll-behavior: smooth;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
height: 200px;
|
||||
flex: 1;
|
||||
}
|
||||
`
|
||||
@@ -0,0 +1,107 @@
|
||||
import type { Clock, ConsoleTarget, Log, NativeMethod, Parser } from './types.ts'
|
||||
import { errorArgument } from './errors.ts'
|
||||
|
||||
interface Listener { callback?: (log: Log) => void }
|
||||
interface SavedMethod { name: string, descriptor?: PropertyDescriptor, wrapper: NativeMethod }
|
||||
interface Owner {
|
||||
listeners: Set<Listener>
|
||||
pending: Set<number>
|
||||
saved: SavedMethod[]
|
||||
feed: { pointers: Record<string, unknown>, src: { npm: string, github: string } }
|
||||
previousFeed?: PropertyDescriptor
|
||||
}
|
||||
|
||||
export function createSubscriptions(target: ConsoleTarget, methods: string[], parse: Parser, clock: Clock) {
|
||||
let owner: Owner | undefined
|
||||
|
||||
function restore(current: Owner) {
|
||||
for (const timer of current.pending)
|
||||
clock.clearTimeout(timer)
|
||||
current.pending.clear()
|
||||
for (const saved of current.saved) {
|
||||
if (target[saved.name] !== saved.wrapper)
|
||||
continue
|
||||
if (saved.descriptor)
|
||||
Object.defineProperty(target, saved.name, saved.descriptor)
|
||||
else
|
||||
delete target[saved.name]
|
||||
}
|
||||
if (target.feed === current.feed) {
|
||||
if (current.previousFeed)
|
||||
Object.defineProperty(target, 'feed', current.previousFeed)
|
||||
else
|
||||
delete target.feed
|
||||
}
|
||||
}
|
||||
|
||||
return (callback: (log: Log) => void) => {
|
||||
if (!owner) {
|
||||
const current: Owner = {
|
||||
listeners: new Set(),
|
||||
pending: new Set(),
|
||||
saved: [],
|
||||
previousFeed: Object.getOwnPropertyDescriptor(target, 'feed'),
|
||||
feed: { pointers: {}, src: { npm: 'https://npmjs.com/package/console-feed', github: 'https://github.com/samdenty99/console-feed' } },
|
||||
}
|
||||
owner = current
|
||||
try {
|
||||
for (const name of methods) {
|
||||
const original = target[name] as NativeMethod
|
||||
const wrapper: NativeMethod = function (...args) {
|
||||
Reflect.apply(original, this, args)
|
||||
const listeners = [...current.listeners]
|
||||
if (!listeners.length)
|
||||
return
|
||||
const timer = clock.setTimeout(() => {
|
||||
current.pending.delete(timer)
|
||||
const log = parse(name, name === 'error' ? args.map(errorArgument) : args)
|
||||
if (log) {
|
||||
let failed = false
|
||||
let failure: unknown
|
||||
for (const listener of listeners) {
|
||||
try {
|
||||
listener.callback?.(log)
|
||||
}
|
||||
catch (error) {
|
||||
if (!failed) {
|
||||
failed = true
|
||||
failure = error
|
||||
}
|
||||
else {
|
||||
clock.setTimeout(() => {
|
||||
throw error
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (failed)
|
||||
throw failure
|
||||
}
|
||||
}, 0)
|
||||
current.pending.add(timer)
|
||||
}
|
||||
current.feed.pointers[name] = original
|
||||
current.saved.push({ name, descriptor: Object.getOwnPropertyDescriptor(target, name), wrapper })
|
||||
target[name] = wrapper
|
||||
}
|
||||
target.feed = current.feed
|
||||
}
|
||||
catch (error) {
|
||||
restore(current)
|
||||
owner = undefined
|
||||
throw error
|
||||
}
|
||||
}
|
||||
const current = owner
|
||||
const listener: Listener = { callback }
|
||||
current.listeners.add(listener)
|
||||
return () => {
|
||||
listener.callback = undefined
|
||||
current.listeners.delete(listener)
|
||||
if (!current.listeners.size && owner === current) {
|
||||
owner = undefined
|
||||
restore(current)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type * as React from 'react'
|
||||
|
||||
export interface Log { method: string, id?: string, data?: unknown[] }
|
||||
export type Parser = (method: string, args: unknown[], id?: string) => Log | false
|
||||
export type NativeMethod = (this: unknown, ...args: unknown[]) => unknown
|
||||
export interface ConsoleTarget { [method: string]: unknown }
|
||||
export interface Clock {
|
||||
setTimeout: (callback: () => void, delay: number) => number
|
||||
clearTimeout: (id: number) => void
|
||||
}
|
||||
export interface Feed { Console: React.ComponentType<{ logs: Log[], variant: string }> }
|
||||
export interface Styled { div: (strings: TemplateStringsArray) => React.ComponentType<React.PropsWithChildren> }
|
||||
export type ReactRuntime = typeof React
|
||||
export interface DomRuntime {
|
||||
render: (element: React.ReactElement, container: Element) => unknown
|
||||
unmountComponentAtNode: (container: Element) => boolean
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { Feed, Log, Parser, ReactRuntime, Styled } from './types.ts'
|
||||
import { css } from './style.ts'
|
||||
import { createSubscriptions } from './subscriptions.ts'
|
||||
|
||||
export function createView(React: ReactRuntime, feed: Feed, styled: Styled, parse: Parser, methods: string[]) {
|
||||
const template = Object.freeze(Object.defineProperty([css], 'raw', { value: Object.freeze([css]) })) as unknown as TemplateStringsArray
|
||||
const Container = styled.div(template)
|
||||
const subscribe = createSubscriptions(window.console as unknown as Record<string, unknown>, methods, parse, window)
|
||||
interface State { logs: Log[], hover: boolean }
|
||||
return class ConsoleView extends React.Component<object, State> {
|
||||
scrollTimer: number | null = null
|
||||
consoleRef = React.createRef<HTMLDivElement>()
|
||||
private unsubscribe?: () => void
|
||||
private mounted = false
|
||||
|
||||
constructor(props: object) {
|
||||
super(props)
|
||||
this.onClear = this.onClear.bind(this)
|
||||
this.onMouseEnter = this.onMouseEnter.bind(this)
|
||||
this.onMouseLeave = this.onMouseLeave.bind(this)
|
||||
this.state = { logs: [], hover: false }
|
||||
}
|
||||
|
||||
override componentDidMount() {
|
||||
this.mounted = true
|
||||
this.unsubscribe = subscribe(log => this.add(log))
|
||||
}
|
||||
|
||||
add(log: Log) {
|
||||
if (!this.mounted)
|
||||
return
|
||||
this.setState(state => ({ logs: [...state.logs, log] }))
|
||||
const element = this.consoleRef.current
|
||||
if (!this.state.hover && element) {
|
||||
if (this.scrollTimer !== null)
|
||||
window.clearTimeout(this.scrollTimer)
|
||||
this.scrollTimer = window.setTimeout(() => {
|
||||
this.scrollTimer = null
|
||||
if (this.mounted && this.consoleRef.current === element)
|
||||
element.scrollTop = element.scrollHeight
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
|
||||
onClear() { this.setState({ logs: [] }) }
|
||||
onMouseEnter() { this.setState({ hover: true }) }
|
||||
onMouseLeave() { this.setState({ hover: false }) }
|
||||
|
||||
override componentWillUnmount() {
|
||||
this.mounted = false
|
||||
this.unsubscribe?.()
|
||||
this.unsubscribe = undefined
|
||||
if (this.scrollTimer !== null)
|
||||
window.clearTimeout(this.scrollTimer)
|
||||
this.scrollTimer = null
|
||||
}
|
||||
|
||||
override render() {
|
||||
const count = React.createElement('span', { className: 'console-header-number' }, this.state.logs.length)
|
||||
const title = React.createElement('div', { className: 'console-header-left' }, 'Console', ' ', count)
|
||||
const clear = React.createElement('div', { className: 'console-header-right', onClick: this.onClear }, 'Clear')
|
||||
const header = React.createElement('div', { className: 'console-header' }, title, clear)
|
||||
const logs = React.createElement(feed.Console, { logs: this.state.logs, variant: 'dark' })
|
||||
const content = React.createElement('div', { ref: this.consoleRef, className: 'console-component', onMouseEnter: this.onMouseEnter, onMouseLeave: this.onMouseLeave }, logs)
|
||||
return React.createElement(Container, null, header, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@
|
||||
"../packages/artplayer-vitepress/build/**/*.ts",
|
||||
"build-vconsole.mjs",
|
||||
"browser-validation/**/*.ts",
|
||||
"../test/browser/chapter-hover.ts"
|
||||
"../test/browser/chapter-hover.ts",
|
||||
"build-console.mjs"
|
||||
]
|
||||
}
|
||||
@@ -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')
|
||||
})
|
||||
@@ -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/)
|
||||
})
|
||||
Reference in new issue
Block a user