refactor(docs): [SITE-03] migrate desktop editor and integrate typed generation

This commit is contained in:
Harvey Zhao committed 2026-09-14 09:25:46 +08:00
1 parent a468cb2c39
commit 2f130c58f1
37 files changed
+1921 -305

No files matched your search

+3
View File
@@ -69,6 +69,9 @@ The repository contains:
- Preserve the existing visual and API style of sibling packages instead of inventing a new pattern.
- When touching demo examples, make sure the example still works in `http://localhost:8082`.
- Do not hand-edit `dist/`, `docs/compiled/`, or `docs/uncompiled/` unless a build step generated them.
- Desktop editor sources live in `packages/artplayer-vitepress/browser/`; `docs/assets/js/common.js`,
`bootstrap.js`, `loader.js`, `mobile.js` and `packages/artplayer-vitepress/docs/public/main.js`
are generated with `yarn build:site-assets`. `editor-libraries.ts` is generated by `yarn build:ts`.
## Primary Commands
+42
View File
@@ -0,0 +1,42 @@
// Generated by yarn build:site-assets. Do not edit.
"use strict";
(() => {
// packages/artplayer-vitepress/browser/editor-preferences.ts
function readPreference(key, storage = () => localStorage) {
try {
return storage().getItem(key) === "true";
} catch {
return false;
}
}
// packages/artplayer-vitepress/browser/bootstrap.ts
function loadScript(url) {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
const clear = () => {
script.onload = null;
script.onerror = null;
};
script.onload = () => {
clear();
resolve();
};
script.onerror = () => {
clear();
script.remove();
reject(new Error(`Editor bootstrap failed: ${url}`));
};
script.src = url;
document.body.appendChild(script);
});
}
async function bootstrap() {
await loadScript(
readPreference("prod") ? "./compiled/artplayer.js" : "./uncompiled/artplayer/index.js"
);
await loadScript("./assets/js/vs/loader.js");
await loadScript("./assets/js/common.js");
}
void bootstrap().catch((error) => console.error(error));
})();
+408 -237
View File
@@ -1,245 +1,416 @@
/* global Artplayer, monaco */
(function () {
Artplayer.DEBUG = true
if (Artplayer.utils.isMobile) {
window.location.href = `./mobile.html${location.search}`
// Generated by yarn build:site-assets. Do not edit.
"use strict";
(() => {
// packages/artplayer-vitepress/browser/editor-files.ts
function readEditorFile(file, signal, createReader = () => new FileReader()) {
return new Promise((resolve, reject) => {
const reader = createReader();
function clear() {
reader.onload = null;
reader.onerror = null;
reader.onabort = null;
signal.removeEventListener("abort", cancel);
}
function fail(error) {
clear();
reject(error);
}
function cancel() {
reader.abort();
fail(new Error(`File import cancelled: ${file.name}`));
}
reader.onload = () => {
if (typeof reader.result !== "string") {
fail(new Error(`File is not text: ${file.name}`));
return;
}
const value = reader.result;
clear();
resolve(value);
};
reader.onerror = () => fail(reader.error || new Error(`Reading file failed: ${file.name}`));
reader.onabort = () => fail(new Error(`File import cancelled: ${file.name}`));
signal.addEventListener("abort", cancel, { once: true });
try {
if (signal.aborted)
cancel();
else reader.readAsText(file);
} catch (error) {
fail(error);
}
});
}
const $codeMirror = document.querySelector('.codeMirrorWrap')
const $lib = document.querySelector('.libsInput')
const $run = document.querySelector('.run')
const $popups = document.querySelector('.popups')
const $console = document.querySelector('.console')
const $prod = document.querySelector('#prod')
const $ts = document.querySelector('#ts')
const $code = document.querySelector('#code')
const $log = document.querySelector('#log')
const $file = document.querySelector('#file')
const $editor = document.querySelector('#editor')
window.consoleLog($console)
// Helper functions for localStorage
const getStorageBoolean = key => localStorage.getItem(key) === 'true'
const setStorageBoolean = (key, value) => localStorage.setItem(key, value ? 'true' : 'false')
$prod.checked = getStorageBoolean('prod')
$ts.checked = getStorageBoolean('ts')
$code.checked = getStorageBoolean('code')
$log.checked = getStorageBoolean('log')
if ($code.checked) {
$editor.style.display = 'none'
}
if ($log.checked) {
$console.style.display = 'none'
}
let editor = null
let initialization = 0
require.config({ paths: { vs: './assets/js/vs' } })
require(['vs/editor/editor.main'], async () => {
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
noSemanticValidation: true,
noSyntaxValidation: false,
})
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.ES6,
allowNonTsExtensions: true,
})
const libUris = [
'./assets/ts/artplayer-plugin-ads.d.ts',
'./assets/ts/artplayer-plugin-ambilight.d.ts',
'./assets/ts/artplayer-plugin-asr.d.ts',
'./assets/ts/artplayer-plugin-audio-track.d.ts',
'./assets/ts/artplayer-plugin-auto-thumbnail.d.ts',
'./assets/ts/artplayer-plugin-chapter.d.ts',
'./assets/ts/artplayer-plugin-chromecast.d.ts',
'./assets/ts/artplayer-plugin-danmuku-mask.d.ts',
'./assets/ts/artplayer-plugin-danmuku.d.ts',
'./assets/ts/artplayer-plugin-dash-control.d.ts',
'./assets/ts/artplayer-plugin-document-pip.d.ts',
'./assets/ts/artplayer-plugin-hls-control.d.ts',
'./assets/ts/artplayer-plugin-jassub.d.ts',
'./assets/ts/artplayer-plugin-multiple-subtitles.d.ts',
'./assets/ts/artplayer-plugin-vast.d.ts',
'./assets/ts/artplayer-plugin-vtt-thumbnail.d.ts',
'./assets/ts/artplayer-proxy-canvas.d.ts',
'./assets/ts/artplayer-proxy-mediabunny.d.ts',
'./assets/ts/artplayer-tool-iframe.d.ts',
'./assets/ts/artplayer-tool-thumbnail.d.ts',
'./assets/ts/artplayer.d.ts',
'./assets/ts/artplayer-i18n.d.ts',
]
for (let index = 0; index < libUris.length; index++) {
const libUri = libUris[index]
const libSource = await (await fetch(libUri)).text()
monaco.languages.typescript.javascriptDefaults.addExtraLib(libSource, libUri)
monaco.editor.createModel(libSource, 'typescript', monaco.Uri.parse(libUri))
}
const disposable = monaco.editor.onDidCreateEditor(() => {
disposable.dispose()
setTimeout(initApp, 1000)
})
editor = monaco.editor.create($codeMirror, {
theme: 'vs-dark',
folding: true,
automaticLayout: true,
quickSuggestions: {
other: true,
comments: true,
strings: true,
function createFileImports(document, label) {
const controller = new AbortController();
const elements = [];
let queue = Promise.resolve();
return {
add(files) {
const work = queue.then(async () => {
for (const file of files) {
const name = file.name.toLowerCase().trim();
if (!name.endsWith(".js") && !name.endsWith(".css"))
continue;
const text = await readEditorFile(file, controller.signal);
if (controller.signal.aborted)
return;
const element = document.createElement(
name.endsWith(".css") ? "style" : "script"
);
element.textContent = text;
document.body.appendChild(element);
elements.push(element);
label(name);
}
});
queue = work.catch(() => {
});
return work;
},
model: monaco.editor.createModel(
dispose() {
controller.abort();
for (const element of elements) element.remove();
elements.length = 0;
}
};
}
// packages/artplayer-vitepress/browser/editor-libraries.ts
var libUris = [
"./assets/ts/artplayer-plugin-ads.d.ts",
"./assets/ts/artplayer-plugin-ambilight.d.ts",
"./assets/ts/artplayer-plugin-asr.d.ts",
"./assets/ts/artplayer-plugin-audio-track.d.ts",
"./assets/ts/artplayer-plugin-auto-thumbnail.d.ts",
"./assets/ts/artplayer-plugin-chapter.d.ts",
"./assets/ts/artplayer-plugin-chromecast.d.ts",
"./assets/ts/artplayer-plugin-danmuku-mask.d.ts",
"./assets/ts/artplayer-plugin-danmuku.d.ts",
"./assets/ts/artplayer-plugin-dash-control.d.ts",
"./assets/ts/artplayer-plugin-document-pip.d.ts",
"./assets/ts/artplayer-plugin-hls-control.d.ts",
"./assets/ts/artplayer-plugin-jassub.d.ts",
"./assets/ts/artplayer-plugin-multiple-subtitles.d.ts",
"./assets/ts/artplayer-plugin-vast.d.ts",
"./assets/ts/artplayer-plugin-vtt-thumbnail.d.ts",
"./assets/ts/artplayer-proxy-canvas.d.ts",
"./assets/ts/artplayer-proxy-mediabunny.d.ts",
"./assets/ts/artplayer-tool-iframe.d.ts",
"./assets/ts/artplayer-tool-thumbnail.d.ts",
"./assets/ts/artplayer.d.ts",
"./assets/ts/artplayer-i18n.d.ts"
];
// packages/artplayer-vitepress/browser/editor-monaco.ts
async function createMonaco(host, container, typescript, signal) {
host.require.config({ paths: { vs: "./assets/js/vs" } });
await new Promise(
(resolve, reject) => host.require(["vs/editor/editor.main"], resolve, reject)
);
await new Promise(
(resolve, reject) => host.require(
[
'var art = new Artplayer({',
'\tcontainer: \'.artplayer-app\',',
'\turl: \'/assets/sample/video.mp4\',',
'});',
].join('\n'),
$ts.checked ? 'typescript' : 'javascript',
),
})
})
async function loadLib(libs) {
$lib.value = decodeURIComponent(libs || '')
return window.ArtplayerDocsLoader.loadLibraries(libs)
}
async function runExample(name, current) {
const text = await window.ArtplayerDocsLoader.exampleSource(name)
if (current !== initialization)
return
editor.setValue(text)
runCode()
}
function loadCode(code, example, current) {
if (current !== initialization)
return
if (example)
return runExample(example, current)
if (code) {
editor.setValue(decodeURIComponent(code).trim())
runCode()
}
else {
return runExample('index', current)
}
}
function runCode() {
window.dispatchEvent(new Event('artplayer:example:cleanup'))
// Destroy all existing instances
Artplayer.instances.slice().forEach(art => art.destroy(true))
const value = editor.getValue()
// eslint-disable-next-line no-eval -- Preserve the editor's classic-script Run scope for user-provided examples.
eval(value)
window.art = Artplayer.instances[0]
}
function initApp() {
const current = ++initialization
const urlParams = window.ArtplayerDocsLoader.parameters(window.location.href)
const { code, libs, example } = urlParams
loadLib(libs)
.then(() => loadCode(code, example, current))
.catch((err) => {
console.error('Failed to initialize app:', err)
})
}
function restart() {
const libs = encodeURIComponent($lib.value)
const code = encodeURIComponent(editor.getValue())
const url = `${window.location.origin}${window.location.pathname}?libs=${libs}&code=${code}`
history.pushState(null, null, url)
initApp()
}
function readFile(file) {
return new Promise((resolve) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result)
reader.readAsText(file)
})
}
$run.addEventListener('click', () => {
restart()
})
$popups.addEventListener('click', function (event) {
if (event.target === this) {
this.style.display = 'none'
}
})
$prod.addEventListener('change', () => {
setStorageBoolean('prod', $prod.checked)
window.location.reload()
})
$ts.addEventListener('change', () => {
setStorageBoolean('ts', $ts.checked)
window.location.reload()
})
$code.addEventListener('change', () => {
setStorageBoolean('code', $code.checked)
window.location.reload()
})
$log.addEventListener('change', () => {
setStorageBoolean('log', $log.checked)
window.location.reload()
})
$file.addEventListener('change', async () => {
for (let index = 0; index < $file.files.length; index++) {
const file = $file.files[index]
const name = file.name.toLowerCase().trim()
if (name.endsWith('.css')) {
const text = await readFile(file)
const $style = document.createElement('style')
$style.textContent = text
document.body.appendChild($style)
$lib.value = `\n[${name}]`
$lib.value = $lib.value.trim()
"vs/basic-languages/typescript/typescript",
"vs/basic-languages/javascript/javascript",
"vs/language/typescript/tsMode"
],
resolve,
reject
)
);
if (signal.aborted)
throw new Error("Editor initialization cancelled");
const api = host.monaco;
const types = api.languages.typescript;
const resources = [];
const temporary = /* @__PURE__ */ new Set();
let editor;
let disposed = false;
const dispose = () => {
if (disposed)
return;
disposed = true;
signal.removeEventListener("abort", dispose);
editor?.dispose();
editor = void 0;
for (const model of temporary) model.dispose();
temporary.clear();
for (const resource of resources.splice(0).reverse()) resource.dispose();
};
signal.addEventListener("abort", dispose, { once: true });
try {
for (const defaults of [
types.javascriptDefaults,
types.typescriptDefaults
]) {
defaults.setDiagnosticsOptions({
noSemanticValidation: defaults === types.javascriptDefaults,
noSyntaxValidation: false
});
defaults.setCompilerOptions({
target: types.ScriptTarget.ES2015,
module: types.ModuleKind.None,
allowNonTsExtensions: true,
noEmit: false
});
}
if (name.endsWith('.js')) {
const text = await readFile(file)
const $script = document.createElement('script')
$script.textContent = text
document.body.appendChild($script)
$lib.value = `\n[${name}]`
$lib.value = $lib.value.trim()
for (const uri of libUris) {
const response = await host.fetch(uri, { signal });
if (!response.ok) {
throw new Error(
`Loading editor declaration failed: ${uri} (${response.status})`
);
}
const source = await response.text();
if (signal.aborted)
throw new Error("Editor initialization cancelled");
resources.push(
types.javascriptDefaults.addExtraLib(source, uri),
types.typescriptDefaults.addExtraLib(source, uri)
);
resources.push(
api.editor.createModel(source, "typescript", api.Uri.parse(uri))
);
}
const model = api.editor.createModel(
"var art = new Artplayer({\n container: '.artplayer-app',\n url: '/assets/sample/video.mp4',\n});",
typescript ? "typescript" : "javascript"
);
resources.push(model);
editor = api.editor.create(container, {
theme: "vs-dark",
folding: true,
automaticLayout: true,
quickSuggestions: { other: true, comments: true, strings: true },
model
});
return {
editor,
async compile(source) {
if (disposed)
throw new Error("Editor was disposed");
if (!typescript)
return source;
const model2 = api.editor.createModel(source, "typescript");
temporary.add(model2);
try {
const worker = await (await types.getTypeScriptWorker())(model2.uri);
const file = model2.uri.toString();
const syntax = await worker.getSyntacticDiagnostics(file);
if (syntax.length) {
throw new Error(
`TypeScript syntax error: ${syntax.map((item) => typeof item.messageText === "string" ? item.messageText : item.messageText.messageText).join("; ")}`
);
}
const output = await worker.getEmitOutput(file);
const script = output.outputFiles.find(
(file2) => file2.name.endsWith(".js")
);
if (output.emitSkipped || !script)
throw new Error("TypeScript worker did not emit JavaScript");
return script.text;
} finally {
temporary.delete(model2);
if (!model2.isDisposed())
model2.dispose();
}
},
dispose
};
} catch (error) {
dispose();
throw error;
}
})
}
window.addEventListener('error', (err) => {
console.error(err)
})
window.addEventListener('unhandledrejection', (err) => {
console.error(err)
})
document.addEventListener('keydown', (event) => {
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
event.preventDefault()
restart()
// packages/artplayer-vitepress/browser/editor-preferences.ts
function readPreference(key, storage = () => localStorage) {
try {
return storage().getItem(key) === "true";
} catch {
return false;
}
})
})()
}
function writePreference(key, value, storage = () => localStorage) {
try {
storage().setItem(key, value ? "true" : "false");
return true;
} catch {
return false;
}
}
// packages/artplayer-vitepress/browser/editor-session.ts
function createEditorSession(options) {
let generation = 0;
let disposed = false;
return {
async run({
libs = "",
code,
example
}) {
const current = ++generation;
const active = () => !disposed && current === generation;
if (!active())
return;
await options.loadLibraries(libs);
if (!active())
return;
const source = example ? await options.exampleSource(example) : code ? decodeURIComponent(code).trim() : await options.exampleSource("index");
if (!active())
return;
options.writeCode(source);
const executable = await options.compile(source);
if (active())
options.execute(executable);
},
dispose() {
disposed = true;
generation++;
}
};
}
function executeClassic(code) {
new Function(code)();
}
// packages/artplayer-vitepress/browser/editor.ts
async function installEditor(host) {
const document = host.document;
const element = (selector) => {
const node = document.querySelector(selector);
if (!node)
throw new Error(`Missing editor element: ${selector}`);
return node;
};
host.Artplayer.DEBUG = true;
if (host.Artplayer.utils.isMobile) {
host.location.href = `./mobile.html${host.location.search}`;
return;
}
const run = element(".run");
const libs = element(".libsInput");
const file = element("#file");
const popup = element(".popups");
const output = element(".console");
host.consoleLog(output);
const controller = new AbortController();
const cleanups = [];
const listen = (target, type, callback) => {
target.addEventListener(type, callback);
cleanups.push(() => target.removeEventListener(type, callback));
};
const report = (error) => {
if (!controller.signal.aborted)
host.console.error(error);
};
listen(host, "error", (event) => host.console.error(event));
listen(host, "unhandledrejection", (event) => host.console.error(event));
for (const key of ["prod", "ts", "code", "log"]) {
const checkbox = element(`#${key}`);
checkbox.checked = readPreference(key, () => host.localStorage);
listen(checkbox, "change", () => {
if (writePreference(key, checkbox.checked, () => host.localStorage)) {
host.location.reload();
} else {
checkbox.checked = readPreference(key, () => host.localStorage);
report(
new Error("Browser storage is unavailable; preference was not saved")
);
}
});
}
if (element("#code").checked)
element("#editor").style.display = "none";
if (element("#log").checked)
output.style.display = "none";
const imports = createFileImports(document, (name) => {
libs.value = `[${name}]`;
});
cleanups.push(() => imports.dispose());
cleanups.push(() => {
host.dispatchEvent(new Event("artplayer:example:cleanup"));
for (const art of host.Artplayer.instances.slice()) {
try {
art.destroy(true);
} catch (error) {
host.console.error(error);
}
}
host.art = void 0;
});
listen(file, "change", () => {
void imports.add(Array.from(file.files || [])).catch(report);
});
listen(popup, "click", (event) => {
if (event.target === popup)
popup.style.display = "none";
});
const onPageHide = (event) => {
if (event.persisted)
return;
controller.abort();
for (const cleanup of cleanups.reverse()) {
try {
cleanup();
} catch (error) {
host.console.error(error);
}
}
};
host.addEventListener("pagehide", onPageHide);
cleanups.push(() => host.removeEventListener("pagehide", onPageHide));
run.setAttribute("aria-disabled", "true");
try {
let restart2 = function() {
const code = encodeURIComponent(monaco.editor.getValue());
const dependencies = encodeURIComponent(libs.value);
host.history.pushState(
null,
"",
`${host.location.origin}${host.location.pathname}?libs=${dependencies}&code=${code}`
);
void session.run({ libs: dependencies, code }).catch(report);
};
var restart = restart2;
const monaco = await createMonaco(
host,
element(".codeMirrorWrap"),
element("#ts").checked,
controller.signal
);
const session = createEditorSession({
loadLibraries: (encoded) => {
libs.value = decodeURIComponent(encoded);
return host.ArtplayerDocsLoader.loadLibraries(encoded);
},
exampleSource: (name) => host.ArtplayerDocsLoader.exampleSource(name),
writeCode: (code) => monaco.editor.setValue(code),
compile: monaco.compile,
execute: (code) => {
host.dispatchEvent(new Event("artplayer:example:cleanup"));
for (const art of host.Artplayer.instances.slice()) art.destroy(true);
executeClassic(code);
host.art = host.Artplayer.instances[0];
}
});
cleanups.push(() => session.dispose(), monaco.dispose);
listen(run, "click", restart2);
listen(document, "keydown", (event) => {
const key = event;
if ((key.ctrlKey || key.metaKey) && key.key === "s") {
key.preventDefault();
restart2();
}
});
run.removeAttribute("aria-disabled");
await session.run(host.ArtplayerDocsLoader.parameters(host.location.href));
} catch (error) {
report(error);
}
}
void installEditor(window).catch(
(error) => console.error(error)
);
})();
+1 -23
View File
@@ -230,29 +230,7 @@
</div>
<script src="./assets/js/console.js"></script>
<script src="./assets/js/loader.js"></script>
<script>
var artplayer = document.createElement('script');
var monacoLoader = document.createElement('script');
var common = document.createElement('script');
artplayer.onload = function () {
monacoLoader.src = "./assets/js/vs/loader.js";
document.body.appendChild(monacoLoader);
}
monacoLoader.onload = function () {
common.src = "./assets/js/common.js";
document.body.appendChild(common);
}
if (localStorage.getItem('prod') === 'true') {
artplayer.src = "./compiled/artplayer.js"
} else {
artplayer.src = "./uncompiled/artplayer/index.js"
}
document.body.appendChild(artplayer);
</script>
<script src="./assets/js/bootstrap.js"></script>
<script>
var isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
+6 -4
View File
@@ -40,12 +40,12 @@
"test:dash-control": "node --test test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js",
"dev": "npx cross-env NODE_ENV=development node ./scripts/dev.js",
"build": "npx cross-env NODE_ENV=production node ./scripts/build.js",
"lint": "eslint \"packages/*/{src,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"packages/artplayer-vitepress/{browser,build}/**/*.ts\" \"scripts/{docs-smoke,editor-declarations,documentation,site-build}/**/*.ts\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" \"playwright*.config.js\" --no-fix docs/assets/js/common.js",
"lint": "eslint \"packages/*/{src,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"packages/artplayer-vitepress/{browser,build}/**/*.ts\" \"scripts/{docs-smoke,editor-declarations,documentation,site-build}/**/*.ts\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" \"playwright*.config.js\" --no-fix",
"build:all": "yarn ci:build && yarn lint",
"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}/**/*.ts\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" \"playwright*.config.js\" --fix docs/assets/js/common.js",
"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}/**/*.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: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: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 lint && yarn typecheck:docs-tools && yarn check:docs-smoke && yarn check:editor-types && yarn check:llm && 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:test && yarn build:docs && yarn test:imports",
@@ -126,7 +126,8 @@
"check:site-assets": "node scripts/build-site-assets.mjs --check",
"typecheck:site-assets": "node node_modules/typescript/bin/tsc -p packages/artplayer-vitepress/browser/tsconfig.json --noEmit",
"check:llm": "node scripts/build-llm.js --check",
"test:site-build": "node --test test/site-build.test.js test/build-docs.test.js test/site-markdown.test.js"
"test:site-build": "node --test test/site-build.test.js test/build-docs.test.js test/site-markdown.test.js",
"test:site-editor": "node --test test/site-editor.test.js"
},
"browserslist": "last 1 Chrome version",
"devDependencies": {
@@ -153,6 +154,7 @@
"lerna": "8.2.4",
"less": "4.5.1",
"linkedom": "0.18.13",
"monaco-editor": "0.30.1",
"pngjs": "7.0.0",
"prompts": "2.4.2",
"react": "19.1.1",
+5 -4
View File
@@ -23,10 +23,11 @@ use their package README, architecture notes and linked editor examples;
navigation links alone are not complete API documentation.
Site-owned browser behavior now lives in `browser/`; its README maps the shared
loader, mobile entry and Run Code/language navigation. `docs/public/main.js` is a
loader, desktop editor, mobile entry and Run Code/language navigation. `docs/public/main.js` is a
generated input to VitePress. Run root `yarn build:site-assets` after changes, or
`yarn check:site-assets` for drift. The existing desktop common.js UI remains a
separate source pending SITE-03 migration; no player package APIs move here.
`yarn check:site-assets` for drift. Desktop common.js and bootstrap.js are generated
from typed modules, with Monaco models/emit, Run ordering, file imports, preferences
and page cleanup separated by responsibility. No player package APIs move here.
## Related tooling
@@ -87,7 +88,7 @@ Generated `main.js` installs one document click listener under
editor and recognizes localhost, 127.0.0.1 and IPv6 loopback. First-language
navigation preserves existing English counterparts and explicit later choices;
unavailable storage keeps the requested page. See `browser/README.md` for the
current rules and verified scope. Full VitePress lifecycle remains SITE-03/05.
current rules and verified scope. Full site/search acceptance remains SITE-05.
## Delivery and remaining acceptance
+43 -8
View File
@@ -11,13 +11,24 @@ Node 24.21.0 / Yarn 1.22.22 toolchain from the repository root.
| `navigation.ts` | Run Code DOM delegation, URL encoding, loopback targets and first-language navigation |
| `../../../scripts/build-site-assets.mjs` | Deterministic ES2020 bundles, available English routes and read-only drift check |
Generated outputs are `docs/assets/js/loader.js`, `docs/assets/js/mobile.js` and
this workspace's `docs/public/main.js`. Do not hand-edit them. `docs/index.html`
loads the desktop bridge before the old core/Monaco/common bootstrap. Mobile
Generated outputs are `docs/assets/js/bootstrap.js`, `common.js`, `loader.js`,
`mobile.js` and this workspace's `docs/public/main.js`. Do not hand-edit them.
`docs/index.html` loads the console bridge and loader before the typed bootstrap. Mobile
bundles the same loader module locally; it does not depend on the desktop bridge.
`docs/assets/js/common.js` remains the desktop UI source and owns Monaco, controls,
file import, editor state and instance cleanup. Its remaining TS/UI migration is
tracked under SITE-03; this task does not claim that whole file was migrated.
`common.js` is now generated from `editor.ts`. Desktop responsibilities are:
- `bootstrap.ts` and `editor-preferences.ts`: core/AMD/editor boot order, denied
storage defaults and the exact prod/ts/code/log preference keys.
- `editor.ts` and `editor-host.ts`: DOM bindings, page lifetime and the explicit
player/AMD/console boundary. Monaco globals come from the HTML bootstrap.
- `editor-monaco.ts`: model/declaration ownership, AMD language readiness and TS emit.
- `editor-session.ts`: latest-run ordering, source priority and classic-script execution.
- `editor-files.ts`: serialized JS/CSS imports, FileReader errors/cancellation and nodes.
- `editor-libraries.ts`: generated declaration URLs, authored through build:ts.
Root `monaco-editor@0.30.1` is a development type dependency matching the existing
browser vendor version. It is imported only as types, never bundled to replace
the browser assets. It does not complete the separate vendor provenance review.
The loader serializes all batches in one page. Each dependency completes before
the next is inserted; successful absolute URLs are cached even if a later resource
@@ -41,6 +52,28 @@ acceptance remains a separate environment check, not inferred from the branch.
Desktop request generations prevent a late example fetch from overwriting a newer
Run. Existing `artplayer:example:cleanup`, instance destruction and controls remain.
Successful preference changes keep their old true/false strings and reload behavior;
code/log visibility and URL pushState/Ctrl/Cmd-S remain. Denied storage defaults to
false, while failed writes report an error and restore the checkbox. Run is unavailable
until initialization finishes. Actual JS/TS AMD modules are awaited before example
dependencies can mask define; the old arbitrary one-second delay is removed.
TypeScript mode now compiles an immutable temporary model before Run. Syntax failures
keep the current player; semantic diagnostics are visible but are not a runtime gate.
This is a classic-script editor, not a module bundler or npm import resolver. JavaScript
uses a fresh non-strict function scope: globals work and top-level var stays local to
that Run. Access to the old private common.js closure was not a supported interface.
Late compilation cannot replace a newer Run; temporary models are disposed on settlement
and page disposal. Monaco worker RPCs have no invented cancellation API.
Imports preserve sequential JS/CSS insertion and the last lowercased `[name]` label.
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
removing script nodes. Persisted pagehide keeps state; synthetic lifecycle tests do
not certify physical-device BFCache behavior.
Query parsing preserves raw values (including literal `+`) for one explicit decode;
the last duplicate wins and fragments are not part of query values. Example still
takes priority over code; mobile/index remain their original defaults. Example HTTP
@@ -62,14 +95,16 @@ yarn build:site-assets
yarn check:site-assets
yarn typecheck:site-assets
yarn typecheck:docs-tools
yarn test:site-editor
node --test test/site-loading.test.js
yarn test:browser test/browser/site-loading.spec.js --workers=1
```
Root build:docs regenerates site assets first. ci:check checks their types and drift;
root lint includes the TS modules and legacy common.js. Direct workspace VitePress
root lint includes the TS sources; common.js is checked by generated asset comparison. Direct workspace VitePress
commands still require regenerating these assets after source edits. The full
VitePress build/deploy, i18n and remote translation/LLM tooling remain SITE-03/05.
VitePress build/i18n tools are in scripts/site-build and offline LLM/draft translation
tools are in scripts/documentation. Full site/search and release acceptance remain separate.
Tests use real local pages, Monaco and controlled media, with unrelated remote
analytics/ads fulfilled inertly in test routes. They do not validate those services.
Keep browser verification serial with expensive compiler baselines when local
@@ -0,0 +1,34 @@
import { readPreference } from './editor-preferences.ts'
function loadScript(url: string): Promise<void> {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
const clear = () => {
script.onload = null
script.onerror = null
}
script.onload = () => {
clear()
resolve()
}
script.onerror = () => {
clear()
script.remove()
reject(new Error(`Editor bootstrap failed: ${url}`))
}
script.src = url
document.body.appendChild(script)
})
}
async function bootstrap(): Promise<void> {
await loadScript(
readPreference('prod')
? './compiled/artplayer.js'
: './uncompiled/artplayer/index.js',
)
await loadScript('./assets/js/vs/loader.js')
await loadScript('./assets/js/common.js')
}
void bootstrap().catch(error => console.error(error))
@@ -0,0 +1,82 @@
export function readEditorFile(
file: File,
signal: AbortSignal,
createReader: () => FileReader = () => new FileReader(),
): Promise<string> {
return new Promise((resolve, reject) => {
const reader = createReader()
function clear() {
reader.onload = null
reader.onerror = null
reader.onabort = null
signal.removeEventListener('abort', cancel)
}
function fail(error: unknown) {
clear()
reject(error)
}
function cancel() {
reader.abort()
fail(new Error(`File import cancelled: ${file.name}`))
}
reader.onload = () => {
if (typeof reader.result !== 'string') {
fail(new Error(`File is not text: ${file.name}`))
return
}
const value = reader.result
clear()
resolve(value)
}
reader.onerror = () =>
fail(reader.error || new Error(`Reading file failed: ${file.name}`))
reader.onabort = () =>
fail(new Error(`File import cancelled: ${file.name}`))
signal.addEventListener('abort', cancel, { once: true })
try {
if (signal.aborted)
cancel()
else reader.readAsText(file)
}
catch (error) {
fail(error)
}
})
}
export function createFileImports(
document: Document,
label: (name: string) => void,
) {
const controller = new AbortController()
const elements: HTMLElement[] = []
let queue: Promise<unknown> = Promise.resolve()
return {
add(files: File[]): Promise<void> {
const work = queue.then(async () => {
for (const file of files) {
const name = file.name.toLowerCase().trim()
if (!name.endsWith('.js') && !name.endsWith('.css'))
continue
const text = await readEditorFile(file, controller.signal)
if (controller.signal.aborted)
return
const element = document.createElement(
name.endsWith('.css') ? 'style' : 'script',
)
element.textContent = text
document.body.appendChild(element)
elements.push(element)
label(name)
}
})
queue = work.catch(() => {})
return work
},
dispose(): void {
controller.abort()
for (const element of elements) element.remove()
elements.length = 0
},
}
}
@@ -0,0 +1,36 @@
import type * as Monaco from 'monaco-editor'
import type {
createLibraryLoader,
exampleSource,
parameters,
} from './loader.ts'
export interface EditorPlayer {
destroy: (removeHtml?: boolean) => unknown
}
export interface AmdLoader {
(
modules: string[],
ready: () => void,
failed: (error: unknown) => void,
): void
config: (options: { paths: { vs: string } }) => void
}
export type EditorHost = Window & {
console: Console
Artplayer: {
DEBUG: boolean
utils: { isMobile: boolean }
instances: EditorPlayer[]
}
monaco: typeof Monaco
require: AmdLoader
consoleLog: (element: HTMLElement) => void
ArtplayerDocsLoader: ReturnType<typeof createLibraryLoader> & {
exampleSource: typeof exampleSource
parameters: typeof parameters
}
art: EditorPlayer | undefined
}
export type Editor = Monaco.editor.IStandaloneCodeEditor
@@ -0,0 +1,25 @@
// Generated by yarn build:ts. Do not edit.
export const libUris = [
'./assets/ts/artplayer-plugin-ads.d.ts',
'./assets/ts/artplayer-plugin-ambilight.d.ts',
'./assets/ts/artplayer-plugin-asr.d.ts',
'./assets/ts/artplayer-plugin-audio-track.d.ts',
'./assets/ts/artplayer-plugin-auto-thumbnail.d.ts',
'./assets/ts/artplayer-plugin-chapter.d.ts',
'./assets/ts/artplayer-plugin-chromecast.d.ts',
'./assets/ts/artplayer-plugin-danmuku-mask.d.ts',
'./assets/ts/artplayer-plugin-danmuku.d.ts',
'./assets/ts/artplayer-plugin-dash-control.d.ts',
'./assets/ts/artplayer-plugin-document-pip.d.ts',
'./assets/ts/artplayer-plugin-hls-control.d.ts',
'./assets/ts/artplayer-plugin-jassub.d.ts',
'./assets/ts/artplayer-plugin-multiple-subtitles.d.ts',
'./assets/ts/artplayer-plugin-vast.d.ts',
'./assets/ts/artplayer-plugin-vtt-thumbnail.d.ts',
'./assets/ts/artplayer-proxy-canvas.d.ts',
'./assets/ts/artplayer-proxy-mediabunny.d.ts',
'./assets/ts/artplayer-tool-iframe.d.ts',
'./assets/ts/artplayer-tool-thumbnail.d.ts',
'./assets/ts/artplayer.d.ts',
'./assets/ts/artplayer-i18n.d.ts',
]
@@ -0,0 +1,130 @@
import type { Editor, EditorHost } from './editor-host.ts'
import { libUris } from './editor-libraries.ts'
export async function createMonaco(
host: EditorHost,
container: HTMLElement,
typescript: boolean,
signal: AbortSignal,
) {
host.require.config({ paths: { vs: './assets/js/vs' } })
await new Promise<void>((resolve, reject) =>
host.require(['vs/editor/editor.main'], resolve, reject),
)
await new Promise<void>((resolve, reject) =>
host.require(
[
'vs/basic-languages/typescript/typescript',
'vs/basic-languages/javascript/javascript',
'vs/language/typescript/tsMode',
],
resolve,
reject,
),
)
if (signal.aborted)
throw new Error('Editor initialization cancelled')
const api = host.monaco
const types = api.languages.typescript
const resources: { dispose: () => void }[] = []
const temporary = new Set<ReturnType<typeof api.editor.createModel>>()
let editor: Editor | undefined
let disposed = false
const dispose = () => {
if (disposed)
return
disposed = true
signal.removeEventListener('abort', dispose)
editor?.dispose()
editor = undefined
for (const model of temporary) model.dispose()
temporary.clear()
for (const resource of resources.splice(0).reverse()) resource.dispose()
}
signal.addEventListener('abort', dispose, { once: true })
try {
for (const defaults of [
types.javascriptDefaults,
types.typescriptDefaults,
]) {
defaults.setDiagnosticsOptions({
noSemanticValidation: defaults === types.javascriptDefaults,
noSyntaxValidation: false,
})
defaults.setCompilerOptions({
target: types.ScriptTarget.ES2015,
module: types.ModuleKind.None,
allowNonTsExtensions: true,
noEmit: false,
})
}
for (const uri of libUris) {
const response = await host.fetch(uri, { signal })
if (!response.ok) {
throw new Error(
`Loading editor declaration failed: ${uri} (${response.status})`,
)
}
const source = await response.text()
if (signal.aborted)
throw new Error('Editor initialization cancelled')
resources.push(
types.javascriptDefaults.addExtraLib(source, uri),
types.typescriptDefaults.addExtraLib(source, uri),
)
resources.push(
api.editor.createModel(source, 'typescript', api.Uri.parse(uri)),
)
}
const model = api.editor.createModel(
'var art = new Artplayer({\n\tcontainer: \'.artplayer-app\',\n\turl: \'/assets/sample/video.mp4\',\n});',
typescript ? 'typescript' : 'javascript',
)
resources.push(model)
editor = api.editor.create(container, {
theme: 'vs-dark',
folding: true,
automaticLayout: true,
quickSuggestions: { other: true, comments: true, strings: true },
model,
})
return {
editor,
async compile(source: string): Promise<string> {
if (disposed)
throw new Error('Editor was disposed')
if (!typescript)
return source
const model = api.editor.createModel(source, 'typescript')
temporary.add(model)
try {
const worker = await (await types.getTypeScriptWorker())(model.uri)
const file = model.uri.toString()
const syntax = await worker.getSyntacticDiagnostics(file)
if (syntax.length) {
throw new Error(
`TypeScript syntax error: ${syntax.map(item => (typeof item.messageText === 'string' ? item.messageText : item.messageText.messageText)).join('; ')}`,
)
}
const output = await worker.getEmitOutput(file)
const script = output.outputFiles.find(file =>
file.name.endsWith('.js'),
)
if (output.emitSkipped || !script)
throw new Error('TypeScript worker did not emit JavaScript')
return script.text
}
finally {
temporary.delete(model)
if (!model.isDisposed())
model.dispose()
}
},
dispose,
}
}
catch (error) {
dispose()
throw error
}
}
@@ -0,0 +1,27 @@
export type Preference = 'prod' | 'ts' | 'code' | 'log'
export function readPreference(
key: Preference,
storage: () => Storage = () => localStorage,
): boolean {
try {
return storage().getItem(key) === 'true'
}
catch {
return false
}
}
export function writePreference(
key: Preference,
value: boolean,
storage: () => Storage = () => localStorage,
): boolean {
try {
storage().setItem(key, value ? 'true' : 'false')
return true
}
catch {
return false
}
}
@@ -0,0 +1,51 @@
interface SessionOptions {
loadLibraries: (encoded: string) => Promise<unknown>
exampleSource: (name: string) => Promise<string>
writeCode: (source: string) => void
compile: (source: string) => Promise<string>
execute: (code: string) => void
}
export function createEditorSession(options: SessionOptions) {
let generation = 0
let disposed = false
return {
async run({
libs = '',
code,
example,
}: {
libs?: string
code?: string
example?: string
}): Promise<void> {
const current = ++generation
const active = () => !disposed && current === generation
if (!active())
return
await options.loadLibraries(libs)
if (!active())
return
const source = example
? await options.exampleSource(example)
: code
? decodeURIComponent(code).trim()
: await options.exampleSource('index')
if (!active())
return
options.writeCode(source)
const executable = await options.compile(source)
if (active())
options.execute(executable)
},
dispose(): void {
disposed = true
generation++
},
}
}
export function executeClassic(code: string): void {
// eslint-disable-next-line no-new-func -- The existing editor explicitly executes user-entered classic script on Run.
new Function(code)()
}
@@ -0,0 +1,152 @@
import type { EditorHost } from './editor-host.ts'
import { createFileImports } from './editor-files.ts'
import { createMonaco } from './editor-monaco.ts'
import { readPreference, writePreference } from './editor-preferences.ts'
import { createEditorSession, executeClassic } from './editor-session.ts'
export async function installEditor(host: EditorHost): Promise<void> {
const document = host.document
const element = <T extends HTMLElement>(selector: string): T => {
const node = document.querySelector<T>(selector)
if (!node)
throw new Error(`Missing editor element: ${selector}`)
return node
}
host.Artplayer.DEBUG = true
if (host.Artplayer.utils.isMobile) {
host.location.href = `./mobile.html${host.location.search}`
return
}
const run = element<HTMLButtonElement>('.run')
const libs = element<HTMLTextAreaElement>('.libsInput')
const file = element<HTMLInputElement>('#file')
const popup = element<HTMLDivElement>('.popups')
const output = element<HTMLDivElement>('.console')
host.consoleLog(output)
const controller = new AbortController()
const cleanups: (() => void)[] = []
const listen = (
target: EventTarget,
type: string,
callback: EventListener,
) => {
target.addEventListener(type, callback)
cleanups.push(() => target.removeEventListener(type, callback))
}
const report = (error: unknown) => {
if (!controller.signal.aborted)
host.console.error(error)
}
listen(host, 'error', event => host.console.error(event))
listen(host, 'unhandledrejection', event => host.console.error(event))
for (const key of ['prod', 'ts', 'code', 'log'] as const) {
const checkbox = element<HTMLInputElement>(`#${key}`)
checkbox.checked = readPreference(key, () => host.localStorage)
listen(checkbox, 'change', () => {
if (writePreference(key, checkbox.checked, () => host.localStorage)) {
host.location.reload()
}
else {
checkbox.checked = readPreference(key, () => host.localStorage)
report(
new Error('Browser storage is unavailable; preference was not saved'),
)
}
})
}
if (element<HTMLInputElement>('#code').checked)
element('#editor').style.display = 'none'
if (element<HTMLInputElement>('#log').checked)
output.style.display = 'none'
const imports = createFileImports(document, (name) => {
libs.value = `[${name}]`
})
cleanups.push(() => imports.dispose())
cleanups.push(() => {
host.dispatchEvent(new Event('artplayer:example:cleanup'))
for (const art of host.Artplayer.instances.slice()) {
try {
art.destroy(true)
}
catch (error) {
host.console.error(error)
}
}
host.art = undefined
})
listen(file, 'change', () => {
void imports.add(Array.from(file.files || [])).catch(report)
})
listen(popup, 'click', (event) => {
if (event.target === popup)
popup.style.display = 'none'
})
const onPageHide = (event: PageTransitionEvent) => {
if (event.persisted)
return
controller.abort()
for (const cleanup of cleanups.reverse()) {
try {
cleanup()
}
catch (error) {
host.console.error(error)
}
}
}
host.addEventListener('pagehide', onPageHide)
cleanups.push(() => host.removeEventListener('pagehide', onPageHide))
run.setAttribute('aria-disabled', 'true')
try {
const monaco = await createMonaco(
host,
element('.codeMirrorWrap'),
element<HTMLInputElement>('#ts').checked,
controller.signal,
)
const session = createEditorSession({
loadLibraries: (encoded) => {
libs.value = decodeURIComponent(encoded)
return host.ArtplayerDocsLoader.loadLibraries(encoded)
},
exampleSource: name => host.ArtplayerDocsLoader.exampleSource(name),
writeCode: code => monaco.editor.setValue(code),
compile: monaco.compile,
execute: (code) => {
host.dispatchEvent(new Event('artplayer:example:cleanup'))
for (const art of host.Artplayer.instances.slice()) art.destroy(true)
executeClassic(code)
host.art = host.Artplayer.instances[0]
},
})
cleanups.push(() => session.dispose(), monaco.dispose)
function restart() {
const code = encodeURIComponent(monaco.editor.getValue())
const dependencies = encodeURIComponent(libs.value)
host.history.pushState(
null,
'',
`${host.location.origin}${host.location.pathname}?libs=${dependencies}&code=${code}`,
)
void session.run({ libs: dependencies, code }).catch(report)
}
listen(run, 'click', restart)
listen(document, 'keydown', (event) => {
const key = event as KeyboardEvent
if ((key.ctrlKey || key.metaKey) && key.key === 's') {
key.preventDefault()
restart()
}
})
run.removeAttribute('aria-disabled')
await session.run(host.ArtplayerDocsLoader.parameters(host.location.href))
}
catch (error) {
report(error)
}
}
// HTML bootstraps the existing player, Monaco AMD loader and console bridge before this entry.
void installEditor(window as unknown as EditorHost).catch(error =>
console.error(error),
)
@@ -0,0 +1,169 @@
{
"task": "SITE-03",
"baseline": "a468cb2c39e626f7b4f737d0ca60334150deae2b",
"node": "v24.21.0",
"yarn": "1.22.22",
"logs": {
"unit": {
"file": "refactor/.cache/site-editor-tests-final.log",
"sha256": "d4189f4e94db6698546a3244198cc683526e18e1db9af7eb2c5cf6f328588681",
"exitCode": 0,
"pass": 5
},
"integration": {
"file": "refactor/.cache/site-editor-integration.log",
"sha256": "a704c43adbef4e0c626d605257da47d928ad3766e98ca803ae616450f39c2eae",
"exitCode": 0,
"pass": 21
},
"baseline": {
"file": "refactor/.cache/site-editor-test-baseline.log",
"sha256": "91e014ad170d1240cef8f52b1399e8239a78d09c036e0e360a32285974f71391",
"exitCode": 0,
"pass": 522
},
"ci": {
"file": "refactor/.cache/site-editor-test-ci.log",
"sha256": "5548f2543f5cf97b1264bd09f0fd40f9d01811c9cca5ce38ffab42628b116b5a",
"exitCode": 0,
"pass": 50
},
"lint": {
"file": "refactor/.cache/site-editor-lint.log",
"sha256": "1067b3ccb58d3adab1bf690433d1174eb224143338b9735ecf58461b0c67f818",
"exitCode": 0
},
"toolchain": {
"file": "refactor/.cache/site-editor-check-toolchain.log",
"sha256": "2afc9e0561f660653ca354105298d2a161162db823ee2b9d35bbdbd5557a2e27",
"exitCode": 0
},
"browserTypes": {
"file": "refactor/.cache/site-editor-browser-types.log",
"sha256": "e660a669a597da32a881a1b6fbc85700acf2b890f3dfb080037e08de1b0b5261",
"exitCode": 0
},
"toolTypes": {
"file": "refactor/.cache/site-editor-tool-types.log",
"sha256": "3423554fc39544b5338814ffda828bda8b71ccd5841a86be422892408819dfa0",
"exitCode": 0
},
"assets": {
"file": "refactor/.cache/site-editor-check-site-assets.log",
"sha256": "0d1fec774e30162af0e6ae5c7bcec5f8f859835f5a0c1ffebe7ccf77ddb0d81d",
"exitCode": 0
},
"declarations": {
"file": "refactor/.cache/site-editor-check-editor-types.log",
"sha256": "03b2fe8bf2c1b1d2641c39ed28348ad1b2079d644ec57e3c3e1eee8c35240073",
"exitCode": 0
},
"llm": {
"file": "refactor/.cache/site-editor-check-llm.log",
"sha256": "e1163bb3dbc38eb5ca3b0d497df6022d14d539452441d288d7a592898562141b",
"exitCode": 0
},
"frozenInstall": {
"file": "refactor/.cache/site-editor-frozen-install.log",
"sha256": "4d587ec6212d0b874a96da55ebe55670fbd4c9e41642d79459d2b3bae0d87c85",
"exitCode": 0
},
"generation": {
"file": "refactor/.cache/site-editor-build-types.log",
"sha256": "6a40248ab3422b04f84abed19b3178cb42acdef76f6a51fd5ca7badc27fd667e",
"exitCode": 0
},
"browser": {
"file": "refactor/.cache/site-editor-browser-accepted.log",
"sha256": "d98249aedb874269171f761dcb52444d2062c8e88ea784ef208abd2d077f7900",
"exitCode": 0
}
},
"dependency": {
"name": "monaco-editor",
"version": "0.30.1",
"category": "root devDependency; type-only",
"oldLockEntriesIdentical": true,
"vendorRuntimeUnchanged": true,
"frozenInstallIgnoredScripts": true
},
"browser": {
"file": "refactor/.cache/site-editor-browser-accepted/report.json",
"sha256": "294110ebf4005af8ed41c9be4197ca917309ed0329a1263cdfdde8e2df2cc421",
"stats": {
"startTime": "2026-09-14T01:16:27.513Z",
"duration": 31682.62,
"expected": 42,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"versions": {
"chromium": "153.0.8010.12",
"firefox": "155.0",
"webkit": "26.6"
},
"platform": "win32",
"note": "Actual local Monaco/player with controlled media; no manual Chrome session or physical-device acceptance"
},
"initialFailure": {
"file": "refactor/.cache/site-editor-browser-initial/report.json",
"sha256": "ba6291978c9143a67903fe4c292a82fe0b6afc3c8b7a47572b4c9a4540ac7860",
"stats": {
"startTime": "2026-09-14T01:03:18.570Z",
"duration": 38071.753,
"expected": 33,
"skipped": 0,
"unexpected": 3,
"flaky": 0
},
"causes": [
"Two Firefox Error display-string assertions; actual Error.message used in candidate tests",
"Real Monaco lazy language AMD race; await modules before starting dependencies"
]
},
"unchangedPaths": [
"packages/artplayer/src",
"packages/artplayer/public",
"packages/artplayer/types",
"docs/assets/ts",
"docs/assets/js/vs",
"docs/document",
"docs/llms.txt"
],
"fingerprints": {
"docs/assets/js/bootstrap.js": "a31b05dfda0ffd9011af4fd786d01fb4b81600d34741c944d1f0144c187e357d",
"packages/artplayer-vitepress/browser/bootstrap.ts": "134128a545375635a05fae4f1dc5707bcca389e7f2db27dcffb17692a1c728fa",
"packages/artplayer-vitepress/browser/editor-files.ts": "09e437a9f70ccc5c4f071cfe6f8a90e11085caa8c26e7707b26dc6d2075a97a4",
"packages/artplayer-vitepress/browser/editor-host.ts": "05673be0ca9aea7d890aa4cd84ab372b7d34407bd8ab674f2c0f619996586f03",
"packages/artplayer-vitepress/browser/editor-libraries.ts": "da64881b70b187dbc0006937c2bc3759b7aeafda860c1b5c631308ebcb2c15a7",
"packages/artplayer-vitepress/browser/editor-monaco.ts": "f51dd01c42a0652a830ae7257ba4432c5c970b91e1f5db585fbbe16d355e004d",
"packages/artplayer-vitepress/browser/editor-preferences.ts": "10806adf43737cf0b85de91d1ca4ed988c192ccc6786129f45792c1bc026bed0",
"packages/artplayer-vitepress/browser/editor-session.ts": "18ddc8161b1c2f0b1aab431625aaf448a222dddec9e01bb7cb741017772cac14",
"packages/artplayer-vitepress/browser/editor.ts": "e27f46bb84abc363a36a3239018168d7ec8d22fe8690cf835a2c354ce54443bd",
"test/browser/site-editor.spec.js": "be9a7fdc76a02861041510425fede36b461d8a72b1f47ee14657af91dc221f56",
"test/site-editor.test.js": "ccad4960d9540da9ecc3208727d4478469bc69d8f07edff121b7581ce2d14268",
"docs/assets/js/common.js": "4ccfc0bba7093a45621e2fe0c9b48cb6da0e6101a2b87a5a0f8785611d2d98fc",
"docs/index.html": "bffcef9ee12817d79d41babaa01ea80ba8d44b2aa866b63105f4a453a2722f28",
"package.json": "176aa0d3eb248b342470a6bda49ffa8f4be62c1f1fd7af58baad8268766dd061",
"packages/artplayer-vitepress/browser/README.md": "2280fc2f0e344de008347cc6421e099e45092f76f4b5b01468933552c1dd5a7f",
"packages/artplayer-vitepress/browser/editor-loader.ts": "7cfdc82726d164d450b21680d950ec3f508482f1ab743febee473855248568e1",
"packages/artplayer-vitepress/browser/loader.ts": "d5d5c079854f40025be6af457baba863ab8cd37af59c120e811b7f59e5c160b6",
"packages/artplayer-vitepress/browser/mobile.ts": "beba095b7fda9fb9aa2c5cd50437ea9e186fa76e700e6e79ee8e9eb67fd86d7d",
"packages/artplayer-vitepress/browser/navigation.ts": "1ad87a6bee717f9e62d28c5e6dc382c5c1b43c09c9f10c64e3ef0824f2fbdedc",
"packages/artplayer-vitepress/browser/tsconfig.json": "9a1331ca4b311d66df7aca6e5ff8a8e3647e338931e013ffc7df58aaee8f6a0c",
"scripts/build-site-assets.mjs": "14effc0e354b8ed2d8c6370861181958058b20cb5e9485b7910fef4346cfc5bb",
"scripts/build-ts.js": "153b487736ac9ece1d34bb5fb58c904eb886cb66ce1d7616a1d64a253d991518",
"scripts/documentation/corpus.ts": "f4a977ec9b68c86ffa71164f8fd1263fe5ec0081a38586716a096cdfb86d8005",
"scripts/editor-declarations/generate.ts": "633fc5c495e8146f79e820357a4a80d656eba8f375b383bb0b2d734fcb1d0e5a",
"yarn.lock": "c76ffff6d6098b911487517f231ce758cf1f233ee3b6af79ab6676bf90b869d6"
},
"limitations": [
"Synthetic non-persisted pagehide is not physical BFCache validation",
"Already executed user-script globals and in-flight Monaco worker RPC cannot be undone",
"Semantic TS diagnostics do not block classic-script execution",
"No full example/search/link/vendor-provenance/device/remote-CI/npm acceptance",
"Full baseline precedes final status/risk edits; those receive targeted final checks",
"Root lint retains one existing generated-declaration warning"
]
}
+40 -7
View File
@@ -17913,12 +17913,13 @@
{
"source": "docs/index.html",
"route": "/",
"sha256Lf": "f654fa04c8aea08bc718e96ce2a438f04cc91060b2bb9b2b86f4e1467258fd97",
"sha256Lf": "bffcef9ee12817d79d41babaa01ea80ba8d44b2aa866b63105f4a453a2722f28",
"kind": "demo-or-editor",
"scriptSources": [
"https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-8579453810848662",
"./assets/js/console.js",
"./assets/js/loader.js"
"./assets/js/loader.js",
"./assets/js/bootstrap.js"
],
"status": "not-run",
"owner": "EX-03"
@@ -18102,7 +18103,7 @@
},
{
"file": "scripts/build-ts.js",
"sha256Lf": "1ff2d13f4247bba1818a187eb945a86969f4c4d60245ba5cac8fc0b2b8edd3ba"
"sha256Lf": "153b487736ac9ece1d34bb5fb58c904eb886cb66ce1d7616a1d64a253d991518"
},
{
"file": "scripts/editor-types.mjs",
@@ -18138,7 +18139,7 @@
},
{
"file": "docs/assets/js/common.js",
"sha256Lf": "c18f72dd5489bdee5b7bd0030c716eab08ac7ea7bd253353eaf406fb8ae2bc14"
"sha256Lf": "4ccfc0bba7093a45621e2fe0c9b48cb6da0e6101a2b87a5a0f8785611d2d98fc"
},
{
"file": "docs/assets/js/mobile.js",
@@ -18158,7 +18159,7 @@
},
{
"file": "scripts/build-site-assets.mjs",
"sha256Lf": "58c163e16efa901d227045b468f324ac79ad4ab2f8ab8716f10a8ede62091666"
"sha256Lf": "14effc0e354b8ed2d8c6370861181958058b20cb5e9485b7910fef4346cfc5bb"
},
{
"file": "scripts/docs-smoke/generator.ts",
@@ -18182,7 +18183,7 @@
},
{
"file": "scripts/editor-declarations/generate.ts",
"sha256Lf": "6cf6ee27b2032e9e3a4aef949f2b54ba7bf12e40bed473478f63eb82759c8e5a"
"sha256Lf": "633fc5c495e8146f79e820357a4a80d656eba8f375b383bb0b2d734fcb1d0e5a"
},
{
"file": "scripts/editor-declarations/plugin.ts",
@@ -18206,7 +18207,7 @@
},
{
"file": "scripts/documentation/corpus.ts",
"sha256Lf": "f916f63822b9e53a26d2017c0004a576c1fbdf0af0f839c296c8e3d2a14bb2c1"
"sha256Lf": "f4a977ec9b68c86ffa71164f8fd1263fe5ec0081a38586716a096cdfb86d8005"
},
{
"file": "scripts/documentation/files.ts",
@@ -18240,10 +18241,42 @@
"file": "packages/artplayer-vitepress/build/markdown.ts",
"sha256Lf": "6d639e25bab97c973940661e2aac6f1034ab53ab1c016310a571ab52392819da"
},
{
"file": "packages/artplayer-vitepress/browser/bootstrap.ts",
"sha256Lf": "134128a545375635a05fae4f1dc5707bcca389e7f2db27dcffb17692a1c728fa"
},
{
"file": "packages/artplayer-vitepress/browser/editor-files.ts",
"sha256Lf": "09e437a9f70ccc5c4f071cfe6f8a90e11085caa8c26e7707b26dc6d2075a97a4"
},
{
"file": "packages/artplayer-vitepress/browser/editor-host.ts",
"sha256Lf": "05673be0ca9aea7d890aa4cd84ab372b7d34407bd8ab674f2c0f619996586f03"
},
{
"file": "packages/artplayer-vitepress/browser/editor-libraries.ts",
"sha256Lf": "da64881b70b187dbc0006937c2bc3759b7aeafda860c1b5c631308ebcb2c15a7"
},
{
"file": "packages/artplayer-vitepress/browser/editor-loader.ts",
"sha256Lf": "7cfdc82726d164d450b21680d950ec3f508482f1ab743febee473855248568e1"
},
{
"file": "packages/artplayer-vitepress/browser/editor-monaco.ts",
"sha256Lf": "f51dd01c42a0652a830ae7257ba4432c5c970b91e1f5db585fbbe16d355e004d"
},
{
"file": "packages/artplayer-vitepress/browser/editor-preferences.ts",
"sha256Lf": "10806adf43737cf0b85de91d1ca4ed988c192ccc6786129f45792c1bc026bed0"
},
{
"file": "packages/artplayer-vitepress/browser/editor-session.ts",
"sha256Lf": "18ddc8161b1c2f0b1aab431625aaf448a222dddec9e01bb7cb741017772cac14"
},
{
"file": "packages/artplayer-vitepress/browser/editor.ts",
"sha256Lf": "e27f46bb84abc363a36a3239018168d7ec8d22fe8690cf835a2c354ce54443bd"
},
{
"file": "packages/artplayer-vitepress/browser/loader.ts",
"sha256Lf": "d5d5c079854f40025be6af457baba863ab8cd37af59c120e811b7f59e5c160b6"
@@ -0,0 +1,89 @@
# SITE-03: desktop editor and generation integration
Baseline: `a468cb2c39e626f7b4f737d0ca60334150deae2b` on
`codex/compatible-modernization`. This completes the remaining desktop portion
of SITE-03; SITE-LOAD-01, SITE-AI-DOCS-01 and SITE-BUILD-01 retain the evidence
for loaders/navigation, translation/LLM tools and i18n/VitePress builds.
## Implementation and ownership
The desktop editor now has strict TypeScript modules for HTML bootstrap, host
types, preferences, Monaco initialization/compilation, run sessions and file
imports. The entry binds existing controls and owns page listeners and cleanup.
The module map and maintenance commands live in
`packages/artplayer-vitepress/browser/README.md`.
`bootstrap.js` and `common.js` are generated from TS alongside the existing three
site assets. Declaration generation owns `browser/editor-libraries.ts`; the UI,
corpus and inventory consume that canonical list. Successful `build:ts` rebuilds
the assets. Read-only declaration and asset checks separately reject drift.
Generated code is checked against its source instead of being hand-linted/fixed.
Root development dependency `monaco-editor@0.30.1` supplies types matching the
existing browser vendor. Imports are type-only. Only one lock entry is added;
no vendor runtime upgrade or new consumer dependency is introduced. Frozen Yarn
installation used `--ignore-scripts`, and strict toolchain verification checks
Node 24.21.0, Yarn 1.22.22, 39 pinned tools and 1471 dependency selectors.
`test:site-editor` is also included in the normal Node test chain.
## Reproduced problems and behavior changes
- The frozen old editor evaluates TypeScript syntax as JavaScript. The candidate
uses the actual Monaco TS worker to emit JS before destroying/running a player.
Syntax failure leaves the current player intact. Semantic diagnostics remain
visible but do not become a new mandatory execution gate.
- The frozen FileReader helper cannot settle read errors. Candidate handlers
reject errors/aborts and remove listeners; imports run in file/batch order.
- The frozen inline bootstrap throws when storage is denied. Candidate defaults
still boot the editor; failed preference writes revert the checkbox and report
the failure rather than claiming persistence.
- Removing the old arbitrary one-second delay exposed a real WebKit AMD race:
Monaco's late language script saw `define` suspended by the example loader.
Initialization now awaits the actual language modules before enabling Run or
starting example dependencies. A controlled delayed-module browser test holds
this boundary explicitly.
## Compatibility and limits
Existing DOM classes, controls, `prod/ts/code/log` keys and string values, URL
encoding, example priority, Ctrl/Meta-S, classic JS execution, dependency caching,
Run cleanup and `window.art` remain. JS imports retain script semantics; classic
Run code has global access and function-local `var`. Accidental access to the old
editor's private eval closure is not a public API. Core/plugin runtime sources,
public declarations and distribution contents are outside this change.
Newer runs supersede delayed example/compile results. Non-persisted pagehide
aborts initialization/imports and releases owned models, imports, listeners and
players. Already executed user scripts cannot be undone, and in-flight Monaco
worker RPC has no cancellation promise. A synthetic pagehide verifies cleanup;
real BFCache eviction/restore and physical devices are not claimed.
## Verification and failure history
Exact results, browser versions and log fingerprints are in
`../baselines/site-editor-validation.json`. New Node tests retain frozen old
reproductions and cover run races, failure propagation, FileReader settlement,
storage and cancellation. The browser suite uses actual local Monaco and player
assets with controlled media; it covers emitted TS, repeat Run/Ctrl-S, imports,
preferences, failed declaration initialization and delayed AMD readiness.
The initial 36-case run had 33 passes and 3 failures. Two Firefox assertions
incorrectly inspected the display string of Error objects; they now inspect the
actual Error message. The third failure exposed the AMD race described above.
Initial and subsequent reports are retained separately. Final 42-case runs pass
on Chromium, Firefox and WebKit with no retry, skip or timeout widening.
Full baseline and CI regressions, strict browser/tool types, root lint and
read-only declaration/asset/LLM checks are recorded with their actual results.
The existing generated core-declaration lint warning is retained. Full site
search/link coverage, all examples, advertising SDK services, remote CI and npm
acceptance remain separate tasks; those are not inferred from editor tests.
## Handoff and rollback
Finish SITE-04/05/07 and EX-03 for documentation semantics, whole-site behavior,
vendor/assets provenance and all examples. Plugin compatibility and release
reviews remain open. Revert this task's commit as one unit (TS sources, generated
assets, declaration-list wiring, tests, dependency/lock and task records); do not
restore only generated common.js while retaining the new source list/HTML entry.
This task is committed locally only, without pushing or publishing.
+7
View File
@@ -134,3 +134,10 @@ Yarn Classic 可执行文件,不再调用 npm。入口仍由 `ci:build` 按原
新增 `test:site-build`,其测试进入 `test:node`;构建 TS 模块与 VitePress
稳定代码分组 ID hook 进入根 lint/docs-tools 类型检查。真实本地构建和三浏览器
页面验证见任务变更记录,远端工作流、Pages 与 npm 验收不因此完成。
## SITE-03 编辑器来源与检查
common.js/bootstrap.js 由 browser 下的 TS 生成,源代码进入 typecheck:site-assets
和根 lint,产物由 check:site-assets 比较;不再对生成 common.js 应用源码 lint。
build:ts 生成声明 URL 清单后重建站点浏览器产物,check:editor-types 保持只读。
新增 test:site-editor 并接入 test:node,新增真实 Monaco 交互测试走现有三引擎矩阵。
+3 -2
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 243 项,范围 22 个包及工作区/示例。
状态:todo 59 / doing 15 / blocked 0 / done 169 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 58 / doing 15 / blocked 0 / done 170 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -386,7 +386,7 @@
| SITE-LOAD-01 | artplayer-vitepress, workspace<br>修复文档示例加载顺序与导航边界 | SITE-02 | 共享 TS loader、移动入口与 Run Code/语言路由,保留 URL 和使用方式 | 旧版复现 define 丢失和依赖乱序;真实浏览器验证成功/失败恢复、顺序、去重、编码/优先级及 localhost/127 路由和语言跳转;不以此替代 SITE-03 构建/翻译流程 | M | done |
| SITE-AI-DOCS-01 | artplayer-vitepress, workspace<br>重构可复现 LLM 文档与安全翻译草稿流程 | SITE-LOAD-01, SITE-02 | TS 离线文档聚合、显式远程翻译、草稿校验和可回退应用,保留命令与目标路径 | 默认不读密钥/调用网络;LLM 内容可复现并保留来源;旧翻译破坏性行为有复现;错误/限流/超时/结构损坏/过期/路径越界拒绝且英文文档不丢失;未执行付费翻译不计服务验收 | M | done |
| SITE-BUILD-01 | artplayer, artplayer-vitepress, workspace<br>重构 i18n 与文档构建的暂存、替换和 Yarn 编排 | SITE-AI-DOCS-01 | 严格 TS 构建模块、编译后替换产物与失败回退,固定 Yarn 子进程和旧构建入口 | 旧 i18n 删除后失败有复现;失败不改变既有产物;语言集合、UMD/ESM/历史别名兼容;文档子进程退出码准确并完成真实 VitePress 构建;不隐式翻译、不修改播放器 API | M | done |
| SITE-03 | artplayer-vitepress<br>整理 i18n/文档/LLM 生成流程 | SITE-02, SITE-LOAD-01, SITE-AI-DOCS-01, SITE-BUILD-01 | build-i18n/build-docs/build-llm/trans-docs 的任务边界和错误处理;桌面 common.js 自有 UI 的 TS 迁移与模块拆分 | 原命令兼容、生成可复现,翻译步骤不隐式运行远程服务;覆盖移动 loader 失败恢复 define、脚本依赖顺序、localhost/127.0.0.1 Run Code 目标和语言重定向;桌面剩余 UI 完成 TS 职责拆分并回归运行、导入与持久设置 | M | todo |
| SITE-03 | artplayer-vitepress<br>整理 i18n/文档/LLM 生成流程 | SITE-02, SITE-LOAD-01, SITE-AI-DOCS-01, SITE-BUILD-01 | build-i18n/build-docs/build-llm/trans-docs 的任务边界和错误处理;桌面 common.js 自有 UI 的 TS 迁移与模块拆分 | 原命令兼容、生成可复现,翻译步骤不隐式运行远程服务;覆盖移动 loader 失败恢复 define、脚本依赖顺序、localhost/127.0.0.1 Run Code 目标和语言重定向;桌面剩余 UI 完成 TS 职责拆分并回归运行、导入与持久设置 | M | done |
| SITE-04 | artplayer-vitepress<br>交叉核对逐包持续维护的文档 | CORE-21, SITE-03, PKG-CHAPTER-04, PKG-AMBILIGHT-04, PKG-AUDIO-04, PKG-AUTO-THUMB-04, PKG-VTT-THUMB-04, PKG-HLS-04, PKG-DASH-04, PKG-MULTI-SUB-04, PKG-JASSUB-04, PKG-MASK-04, PKG-ASR-04, PKG-ADS-04, PKG-VAST-04, PKG-CAST-04, PKG-DPIP-04, PKG-CANVAS-04, PKG-IFRAME-04, PKG-TOOL-THUMB-04, PKG-DANMUKU-06, PKG-MB-08 | 已随实现更新的中文/英文 API、包内实现地图、旧 JS 示例及已知能力限制的全包核对 | 未把缺环境的能力写成已验证,静态核对不等待设备任务;最终 demo 仍由 EX-03 验收;按 SITE-01 声明成员/候选标题清单逐项语义核对,补齐缺失的双语插件说明和 Danmuku 英文入口 | M | todo |
| SITE-05 | artplayer-vitepress<br>构建文档站和验证链接/示例 | SITE-04, EX-01, EX-02, SITE-07 | VitePress 构建、链接与嵌入 demo 检查 | 文档构建、链接、嵌入路径与声明注入通过;真实完整 demo 保留 EX-03 独立门槛;核对 ENG-PM-01 登记的搜索 peer 范围和真实搜索行为 | M | todo |
| SITE-06 | artplayer-vitepress<br>文档站交付验收 | SITE-05 | 维护指南和站点变更记录 | 未手改 generated 目录,旧 URL 可用、部署与检查分离 | M | todo |
@@ -615,6 +615,7 @@
- SITE-LOAD-01: [记录](changes/2026-09-14-SITE-LOAD-01-site-loading.md) [记录](baselines/site-loading-validation.json)
- SITE-AI-DOCS-01: [记录](changes/2026-09-14-SITE-AI-DOCS-01-documentation-pipeline.md) [记录](baselines/documentation-pipeline-validation.json)
- SITE-BUILD-01: [记录](changes/2026-09-14-SITE-BUILD-01-staged-builds.md) [记录](baselines/site-build-validation.json)
- SITE-03: [记录](changes/2026-09-14-SITE-03-desktop-editor.md) [记录](baselines/site-editor-validation.json)
- SITE-07: [记录](site-inventory.md) [记录](baselines/site-provenance.json)
- EX-01: [记录](changes/2026-09-14-EX-01-react-consumer.md) [记录](baselines/react-consumer-validation.json) [记录](scripts/react-consumer.mjs)
- EX-02: [记录](changes/2026-09-14-EX-02-vue-consumer.md) [记录](baselines/vue-consumer-validation.json) [记录](scripts/vue-consumer.mjs)
+16
View File
@@ -1,5 +1,21 @@
# 进度与证据
## SITE-03 桌面编辑器与生成流程完成
桌面自有 UI 拆为严格 TS 的 bootstrap、Monaco、运行会话、文件导入与设置模块;
common.js/bootstrap.js 由源码生成,编辑器声明列表移到生成的 TS 单一来源。
实际 TS worker 编译后运行,修复旧版原样 eval TS、文件读取错误不结算及存储
被拒时无法启动。三浏览器验证发现并修复 Monaco 语言模块与依赖 loader 的
AMD 竞态,保留初始失败。原 DOM、偏好键、URL、Run/Ctrl-S 与公共包 API 保留。
最终浏览器 42、目标集成 21、baseline 522、CI 50 全部通过;严格类型、工具链、
生成一致性、冻结安装和 lint 通过(1 条既有 warning)。只新增 Monaco0.30.1
开发类型依赖,既有锁条目、vendor、核心公开声明未变。合成 pagehide 不代替
真机 BFCache;完整示例/搜索/链接/设备与远端发布验收仍开放。
见[变更](changes/2026-09-14-SITE-03-desktop-editor.md)和
[证据](baselines/site-editor-validation.json)。当前 243 项:170 done、15 doing、
58 todo。下一步 SITE-04 中英文 API 语义核对;Auto Thumbnail/VAST 源码迁移和
全部插件最终验收仍开放。独立本地提交后审计,无推送或发布。
## SITE-BUILD-01 构建流程完成
i18n/VitePress 构建完成严格 TS 模块拆分,暂存后替换并保留失败恢复路径;
+2
View File
@@ -258,3 +258,5 @@
| SITE-TRANS-DATA-01 | resolved / 已复现 | Translation deletes English before requests and repairs valid fences destructively; exhausted rate limits return undefined | SITE-AI-DOCS-01 |
| SITE-I18N-DATA-01 | resolved / 已复现 | Language build deletes existing distribution before compilation succeeds | SITE-BUILD-01 |
| SITE-CODE-GROUP-01 | resolved / 已复现 | VitePress code-group random IDs make identical site inputs emit different output hashes | SITE-BUILD-01 |
| SITE-EDITOR-FAILURE-01 | resolved / 已复现 | Desktop editor evaluates raw TypeScript and cannot settle FileReader errors or boot with denied storage | SITE-03 |
| SITE-EDITOR-AMD-01 | resolved / 已复现 | Late Monaco language AMD initialization races with example dependency loading | SITE-03 |
+48
View File
@@ -5823,6 +5823,54 @@
"test/site-markdown.test.js"
],
"resolutionRationale": "Installed renderer reproduces random IDs; wrapper stabilizes page/group/tab identity and label associations. Consecutive full builds have equal tree fingerprints and three browser engines verify actual tab selection and visible blocks."
},
{
"id": "SITE-EDITOR-FAILURE-01",
"title": "Desktop editor evaluates raw TypeScript and cannot settle FileReader errors or boot with denied storage",
"confirmation": "reproduced",
"status": "resolved",
"owners": [
"SITE-03"
],
"evidence": [
"refactor/changes/2026-09-14-SITE-03-desktop-editor.md",
"refactor/baselines/site-editor-validation.json",
"test/site-editor.test.js",
"test/browser/site-editor.spec.js"
],
"compatibleResolution": "Keep existing UI selectors, preference keys, URL and player/plugin APIs; fix internal initialization, failure and TS execution behavior.",
"closureCriteria": "Retain old failure evidence and verify candidate actual editor interactions plus deterministic generation and strict types.",
"resolutionEvidence": [
"refactor/changes/2026-09-14-SITE-03-desktop-editor.md",
"refactor/baselines/site-editor-validation.json",
"test/site-editor.test.js",
"test/browser/site-editor.spec.js"
],
"resolutionRationale": "Frozen old-source reproductions and candidate Node/browser tests verify TS emit before execution, settled file errors and storage-denied fallback without changing player APIs."
},
{
"id": "SITE-EDITOR-AMD-01",
"title": "Late Monaco language AMD initialization races with example dependency loading",
"confirmation": "reproduced",
"status": "resolved",
"owners": [
"SITE-03"
],
"evidence": [
"refactor/changes/2026-09-14-SITE-03-desktop-editor.md",
"refactor/baselines/site-editor-validation.json",
"test/site-editor.test.js",
"test/browser/site-editor.spec.js"
],
"compatibleResolution": "Keep existing UI selectors, preference keys, URL and player/plugin APIs; fix internal initialization, failure and TS execution behavior.",
"closureCriteria": "Retain old failure evidence and verify candidate actual editor interactions plus deterministic generation and strict types.",
"resolutionEvidence": [
"refactor/changes/2026-09-14-SITE-03-desktop-editor.md",
"refactor/baselines/site-editor-validation.json",
"test/site-editor.test.js",
"test/browser/site-editor.spec.js"
],
"resolutionRationale": "Initial real WebKit failure retained; await JS/TS language modules before Run and example dependencies. Delayed-module regression passes on all three engines."
}
]
}
+1 -1
View File
@@ -77,7 +77,7 @@ export function captureSiteInventory() {
examples: demo.examples,
pages: demo.pages,
baselinePaths: { examplesAdded: demo.examples.filter(row => !historical.examples.some(old => old.source === row.source)).map(row => row.source), examplesRemoved: historical.examples.filter(row => !demo.examples.some(now => now.source === row.source)).map(row => row.source), htmlAdded: demo.pages.filter(row => !historical.pages.some(old => old.source === row.source)).map(row => row.source), htmlRemoved: historical.pages.filter(row => !demo.pages.some(now => now.source === row.source)).map(row => row.source) },
editorDeclarations: [...read('docs/assets/js/common.js').matchAll(/'\.\/assets\/ts\/([^']+\.d\.ts)'/g)].map(match => ({ file: `docs/assets/ts/${match[1]}`, exists: fs.existsSync(path.join(root, 'docs/assets/ts', match[1])), owner: 'SITE-02' })),
editorDeclarations: [...read('packages/artplayer-vitepress/browser/editor-libraries.ts').matchAll(/'\.\/assets\/ts\/([^']+\.d\.ts)'/g)].map(match => ({ file: `docs/assets/ts/${match[1]}`, exists: fs.existsSync(path.join(root, 'docs/assets/ts', match[1])), owner: 'SITE-02' })),
scripts: [...scriptFiles, 'scripts/build-site-assets.mjs', ...files('scripts/docs-smoke').filter(file => file.endsWith('.ts')), ...files('scripts/editor-declarations').filter(file => file.endsWith('.ts')), ...files('scripts/documentation').filter(file => file.endsWith('.ts')), ...files('scripts/site-build').filter(file => file.endsWith('.ts')), ...files('packages/artplayer-vitepress/build').filter(file => file.endsWith('.ts')), ...files('packages/artplayer-vitepress/browser').filter(file => file.endsWith('.ts'))].map(file => ({ file, sha256Lf: hash(read(file)) })),
generationCommands: Object.fromEntries(Object.entries(json('package.json').scripts).filter(([name]) => /^(?:build:(?:types|ts|test|i18n|docs|llm|all)|ci:build)$/.test(name))),
siteManifest: json('packages/artplayer-vitepress/package.json'),
+11 -7
View File
@@ -10,7 +10,8 @@ SITE-SMOKE-01 后续已将 build:test 拆为 TS 解析、生成和浏览器运
SITE-LOAD-01 后续修复并抽取共享 loader、移动入口和 Run Code/语言导航,TS
来源及生成规则见[浏览器模块](../packages/artplayer-vitepress/browser/README.md)。
保留本站点生成/翻译流程和桌面剩余 UI 给 SITE-03,完整内容/页面验收给 SITE-04/05。
SITE-AI-DOCS-01/SITE-BUILD-01 已落实生成、翻译草稿和暂存构建;SITE-03 完成
桌面 UI 的 TS 模块拆分、实际 TS emit 与资源管理。完整内容/页面验收仍给 SITE-04/05。
SITE-01 在 `d62ab13a35c756ea567c426d4fbe00d893dd7e2b` 后核对当前源码。
可重跑清单见 [site-inventory.json](baselines/site-inventory.json);它登记
@@ -57,24 +58,27 @@ SITE-01 在 `d62ab13a35c756ea567c426d4fbe00d893dd7e2b` 后核对当前源码。
`prod` 是 localStorage 键/勾选项,不是已实现的查询参数;只选择核心产物,
不会重写 libs 中的插件路径。`libs` URI 解码后按换行分隔,依扩展名加载 JS/CSS,
Promise.all 等待全部完成;这不保证列表中脚本按顺序执行。
当前 loader 串行加载并缓存成功 URL,失败允许重试,保留列表依赖顺序。
`example` 优先于 `code`;均不存在时桌面取 index.js,移动取 mobile.js。
桌面移动重定向保留 location.search。桌面 Run 先发送 `artplayer:example:cleanup`,
销毁已有实例再 eval;重启将 libs/code 写入 URL。不要把 TS 模式视为严格类型编译证明。
销毁已有实例后在独立经典函数作用域运行;重启将 libs/code 写入 URL。
TS 模式先通过实际 Monaco worker 发射 JavaScript,语法错误保留当前实例;
语义诊断可见但不作为 Run 的阻断条件,不等于发布级严格类型验收。
当前编辑器注入 22 个 d.ts(20 个生态包、核心、i18n),逐个 fetch 后 addExtraLib/createModel。
来源是核心 public 经 build:types/build:ts,以及各生态包 types 经 build:ts。
URL 列表生成在 browser/editor-libraries.ts,build:ts 同步生成 common.js。
仅存在性检查不证明请求成功或 Monaco 诊断有效,真实编辑器验收归 EX-03。
loadScript 暂时覆盖 window.define:桌面成功/失败均恢复,移动失败分支没有恢复;
重复运行和加载失败顺序需要 SITE-03 / EX-03 修复与回归。
loadScript 暂时覆盖 window.define 后在成功/失败/取消时恢复。桌面先等待 Monaco
实际语言模块就绪,避免其 AMD 加载与外部脚本的临时 define 覆盖竞态。
## 生成流程和分发
| 命令/脚本 | 输入 → 输出 | 当前边界/责任 |
| --- | --- | --- |
| build:types | 核心 public → types | 已有严格生成检查,不手改输出 |
| build:ts | 核心及插件声明 → assets/ts、common.js 的 libUris | 大部分语义转换,仍有字符串 fallback;SITE-02 |
| build:test | 中文 Run Code → docs/test/test.js | 排除 en/plugin/public/.vitepress;100ms done 只是 smoke,含生成时间戳;SITE-02 |
| build:ts | 核心及插件声明 → assets/ts、editor-libraries.ts、生成 UI | 两代编译器整组校验;保持原声明 URL 与选包命令 |
| build:test | 中文 Run Code → docs/test/test.js、examples.json | TS 解析生成;按真实 ready/error 清理,覆盖范围见 SITE-SMOKE-01 |
| build:i18n | 核心语言源 → dist/i18n、compiled/i18n | SITE-BUILD-01 暂存全套 UMD/ESM 后替换;保留内置语言/辅助排除 |
| build:docs | VitePress 源 → docs/document | SITE-BUILD-01 固定 Yarn 子进程、暂存构建、失败回退 |
| build:llm | 英文文档、实际编辑器声明、示例、声明 notices → docs/llms.txt + manifest | SITE-AI-DOCS-01 离线可复现;check:llm 只读检查 |
+5 -2
View File
@@ -4223,11 +4223,14 @@
"SITE-AI-DOCS-01",
"SITE-BUILD-01"
],
"status": "todo",
"status": "done",
"risk": "M",
"deliverable": "build-i18n/build-docs/build-llm/trans-docs 的任务边界和错误处理;桌面 common.js 自有 UI 的 TS 迁移与模块拆分",
"acceptance": "原命令兼容、生成可复现,翻译步骤不隐式运行远程服务;覆盖移动 loader 失败恢复 define、脚本依赖顺序、localhost/127.0.0.1 Run Code 目标和语言重定向;桌面剩余 UI 完成 TS 职责拆分并回归运行、导入与持久设置",
"evidence": []
"evidence": [
"changes/2026-09-14-SITE-03-desktop-editor.md",
"baselines/site-editor-validation.json"
]
},
{
"id": "SITE-04",
+5
View File
@@ -34,6 +34,11 @@ CORE-25 默认选项缺陷已修复并通过相同三 Node 与严格打包检查
## 验证记录
SITE-03 新增根开发依赖 `monaco-editor@0.30.1`,仅为桌面编辑器提供与既有
Monaco 浏览器资产匹配的精确 API 类型。只使用 type import,不替换 vendor
或加入播放器产物;root yarn.lock 仅增加该版本的一个条目。新增
`yarn test:site-editor` 验证编辑器状态/读取失败;浏览器检查仍使用原 assets/js/vs。
ENG-01 的 [npm 验证报告](baselines/toolchain-validation.json) 是切换前历史证据,不代表 Yarn 已验证。[Yarn 验证报告](baselines/yarn-validation.json) 记录最终干净冻结安装、20 项 Node 测试、21 库包 63 产物及文档站构建全部通过;63 产物 SHA-256 与 ENG-01 完全相同。保留的旧锁条目版本变化为零,9 个 workspace runtime 依赖解析与 npm 基线一致。搜索 peer 警告由 SITE-05 接续验证。
构建通过不代表运行时、类型、真实浏览器或 npm 发布验收已完成。Chrome 不可用时按 release-reviews.md 使用内置浏览器并注明实际环境。
+1 -1
View File
@@ -11,7 +11,7 @@ const englishPaths = globSync('**/*.md', { cwd: `${root}/docs/en`, posix: true }
/** @type {Map<string, string>} */
const outputs = new Map()
/** @type {[string, string][]} */
const entries = [['editor-loader.ts', 'docs/assets/js/loader.js'], ['mobile.ts', 'docs/assets/js/mobile.js'], ['navigation.ts', `${root}/docs/public/main.js`]]
const entries = [['bootstrap.ts', 'docs/assets/js/bootstrap.js'], ['editor.ts', 'docs/assets/js/common.js'], ['editor-loader.ts', 'docs/assets/js/loader.js'], ['mobile.ts', 'docs/assets/js/mobile.js'], ['navigation.ts', `${root}/docs/public/main.js`]]
for (const [entry, output] of entries) {
const result = await build({
...(entry === 'navigation.ts'
+7
View File
@@ -1,4 +1,11 @@
import { spawnSync } from 'node:child_process'
import process from 'node:process'
import { runEditorDeclarations } from './editor-declarations/generate.ts'
await runEditorDeclarations(process.argv.slice(2))
if (!process.argv.includes('--check')) {
const child = spawnSync(process.execPath, ['scripts/build-site-assets.mjs'], { stdio: 'inherit', windowsHide: true })
if (child.error)
throw child.error
process.exitCode = child.status ?? 1
}
+3 -3
View File
@@ -5,11 +5,11 @@ import { ownedPath, read, sha256 } from './files.ts'
export function buildCorpus(root: string): Map<string, string> {
const common = ts.createSourceFile(
'common.js',
read(ownedPath(root, 'docs/assets/js/common.js')),
'editor-libraries.ts',
read(ownedPath(root, 'packages/artplayer-vitepress/browser/editor-libraries.ts')),
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.JS,
ts.ScriptKind.TS,
)
const declarations: string[] = []
let lists = 0
+14 -3
View File
@@ -22,6 +22,15 @@ Orchestration depends on conversion and validation; conversion does not write.
Public package declarations remain the source of truth. Do not hand-edit
`docs/assets/ts/*.d.ts`.
The editor URL list is generated in
`packages/artplayer-vitepress/browser/editor-libraries.ts`, then imported by the
desktop TypeScript entry. `common.js` is a generated browser bundle. `build:ts`
regenerates site assets after successful writes; package selection arguments still
reach declaration generation. Read-only `check:editor-types` checks the list and
declarations, while `check:site-assets` independently checks the compiled UI.
The LLM corpus and site inventory read this source list to avoid using a stale
compiled UI as the declaration authority.
All selected declarations are formatted in memory using layout-only ESLint fixes,
then checked together with TypeScript 5.9.3 and 4.3.5, without `skipLibCheck` or
ambient dependency discovery. This checks cross-package globals too. SDK methods
@@ -44,7 +53,7 @@ Adjacent generated `artplayer-plugin-vast.LICENSE.txt` preserves upstream notice
This does not settle VAST's runtime default-behavior compatibility decision.
The editor loads 22 declarations: core, 20 ecosystem libraries and i18n. The
unreferenced legacy `artplayer-plugin-websr.d.ts` asset is not in the `common.js`
unreferenced legacy `artplayer-plugin-websr.d.ts` asset is not in the canonical
library list and is not owned or deleted here. Monaco tests follow the actual
list. AST updates require exactly one array-valued `libUris` declaration; missing
or ambiguous declarations fail. Package-only builds leave that list alone.
@@ -66,5 +75,7 @@ yarn test:browser test/browser/editor-declarations.spec.js --workers=1
Browser coverage uses the actual repository Monaco assets, all loaded declarations,
positive/negative consumers and emitted Chapter code against controlled media.
VAST is type-checked only: no ad request or SDK/network/device acceptance. Full
editor UI, routes, example coverage and release reviews remain SITE-03/04/05,
EX-03 and release tasks. SITE-02 installs no new dependencies.
editor UI migration and its targeted interaction checks are recorded in SITE-03;
full routes, example coverage and release reviews remain SITE-04/05, EX-03 and
release tasks. SITE-02 installs no new dependencies; SITE-03 adds matching
Monaco 0.30.1 development types without replacing browser vendor assets.
+5 -2
View File
@@ -64,8 +64,11 @@ export async function generateEditorDeclarations(selected: string[] = []): Promi
for (const compiler of [ts, compat as unknown as typeof ts])
assert.deepEqual(checkStandaloneDeclarations(declarations, compiler), [], `Invalid editor declarations (TS ${compiler.version})`)
if (!selected.length) {
const file = 'docs/assets/js/common.js'
outputs.set(file, editorLibUris(fs.readFileSync(file, 'utf8'), [...pluginFiles.sort(), 'artplayer.d.ts', languageFile]))
const file = 'packages/artplayer-vitepress/browser/editor-libraries.ts'
const code = editorLibUris(fs.readFileSync(file, 'utf8'), [...pluginFiles.sort(), 'artplayer.d.ts', languageFile])
const [result] = await eslint.lintText(code, { filePath: file })
assert(result && !result.errorCount, 'Invalid editor library manifest')
outputs.set(file, result.output || code)
}
if (sources.some(file => pluginIdentity(file).packageName === 'artplayer-plugin-vast')) {
const notices = [['@glomex/vast-ima-player', 'LICENSE'], ['@alugha/ima', 'LICENSE.md']].map(([name, file]) => `${name}\n${fs.readFileSync(path.join('node_modules', name!, file!), 'utf8').replaceAll('\r\n', '\n').trim()}\n`).join('\n')
+9
View File
@@ -168,3 +168,12 @@ environment used to locate the pinned Yarn executable. The Markdown test uses
the installed VitePress renderer to reproduce random code-group IDs and verify
stable IDs, uniqueness and label associations. Built-page browser checks live in
`test/browser/document-site.spec.js`; their editor destination is intercepted.
## Desktop editor regressions
`yarn test:site-editor` covers frozen old TS execution/file error behavior, latest-run
ordering, library/compiler failures, FileReader settlement and storage denial.
`test/browser/site-editor.spec.js` uses the actual Monaco 0.30.1 worker and original
vendor assets for typed Run, Ctrl-S, imports, preferences, declaration failure,
language readiness and model disposal. Console Error objects are inspected through
their arguments: Firefox may serialize their display text as only `Error`.
+1 -1
View File
@@ -3,7 +3,7 @@ import { hash } from '../../refactor/scripts/releases.mjs'
import { expect, test } from './fixtures.js'
test('Monaco checks all editor declarations together and runs the Chapter consumer', async ({ page }, testInfo) => {
const common = fs.readFileSync('docs/assets/js/common.js', 'utf8')
const common = fs.readFileSync('packages/artplayer-vitepress/browser/editor-libraries.ts', 'utf8')
const list = common.match(/(?:let|const) libUris = \[([\s\S]*?)\]/)?.[1] || ''
const names = [...list.matchAll(/'\.\/assets\/ts\/([^']+\.d\.ts)'/g)].map(match => match[1])
expect(names).toHaveLength(22)
+247
View File
@@ -0,0 +1,247 @@
import { Buffer } from 'node:buffer'
import { expect, test } from './fixtures.js'
const player
= 'var art = new Artplayer({ container: ".artplayer-app", url: "/test/pattern.mp4", muted: true });'
async function setup(page) {
await page.route('https://**/*', route =>
route.fulfill({ status: 200, body: '' }))
}
function waitError(page, expected) {
return page.waitForEvent(
'console',
async message =>
message.type() === 'error'
&& message.args().length > 0
&& (
await message
.args()[0]
.evaluate(value =>
value instanceof Error ? value.message : String(value),
)
).includes(expected),
)
}
test('desktop TypeScript mode emits and runs typed code, Ctrl-S reruns and invalid syntax keeps the current player', async ({
page,
}) => {
await setup(page)
await page.addInitScript(() => localStorage.setItem('ts', 'true'))
await page.goto(
`/?code=${encodeURIComponent(`const count: number = 7; window.typedCount = count; ${player}`)}`,
)
await page.waitForFunction(
() => window.art?.isReady && window.typedCount === 7,
)
expect(
await page.evaluate(() => window.monaco.editor.getModels().length),
).toBe(23)
const first = await page.evaluate(() => {
window.previous = window.art
return window.art.id
})
await page.evaluate(() =>
window.monaco.editor
.getModels()
.find(model => !model.uri.path.endsWith('.d.ts'))
.setValue(
'const next: number = 8; window.typedCount = next; var art = new Artplayer({container:".artplayer-app",url:"/test/pattern.mp4",muted:true});',
),
)
await page.keyboard.press('Control+s')
await page.waitForFunction(
id => window.art?.isReady && window.art.id !== id,
first,
)
expect(
await page.evaluate(() => ({
count: window.typedCount,
destroyed: window.previous.isDestroy,
models: window.monaco.editor.getModels().length,
})),
).toEqual({ count: 8, destroyed: true, models: 23 })
await page.evaluate(() => {
window.previous = window.art
window.monaco.editor
.getModels()
.find(model => !model.uri.path.endsWith('.d.ts'))
.setValue('const invalid: = 1')
})
const error = waitError(page, 'TypeScript syntax error')
await page.locator('.run').click()
await error
expect(
await page.evaluate(
() => window.art === window.previous && !window.art.isDestroy,
),
).toBe(true)
await page.evaluate(() =>
window.dispatchEvent(
new PageTransitionEvent('pagehide', { persisted: false }),
),
)
expect(
await page.evaluate(() => window.monaco.editor.getModels().length),
).toBe(0)
})
test('desktop file imports retain order, support CSS and remain usable after Run', async ({
page,
}) => {
await setup(page)
await page.goto(`/?code=${encodeURIComponent(player)}`)
await page.waitForFunction(() => window.art?.isReady)
await page.locator('#file').setInputFiles([
{
name: 'FIRST.JS',
mimeType: 'text/javascript',
buffer: Buffer.from('window.importOrder = [1];'),
},
{
name: 'second.js',
mimeType: 'text/javascript',
buffer: Buffer.from('window.importOrder.push(2);'),
},
{
name: 'custom.css',
mimeType: 'text/css',
buffer: Buffer.from(
'.artplayer-app { --editor-import-marker: imported; }',
),
},
])
await expect(page.locator('.libsInput')).toHaveValue('[custom.css]')
expect(await page.evaluate(() => window.importOrder)).toEqual([1, 2])
expect(
await page
.locator('.artplayer-app')
.evaluate(element =>
getComputedStyle(element)
.getPropertyValue('--editor-import-marker')
.trim(),
),
).toBe('imported')
const before = await page.evaluate(() => window.art.id)
await page.locator('.run').click()
await page.waitForFunction(
id => window.art?.isReady && window.art.id !== id,
before,
)
expect(await page.evaluate(() => window.importOrder)).toEqual([1, 2])
})
test('denied browser storage falls back to a working editor and reports unsaved preferences', async ({
page,
}) => {
await setup(page)
await page.addInitScript(() =>
Object.defineProperty(window, 'localStorage', {
get() {
throw new DOMException('Denied', 'SecurityError')
},
}),
)
await page.goto(`/?code=${encodeURIComponent(player)}`)
await page.waitForFunction(() => window.art?.isReady)
const before = await page.evaluate(() => window.art.id)
const error = waitError(page, 'preference was not saved')
await page.locator('#prod').click()
await error
await expect(page.locator('#prod')).not.toBeChecked()
expect(await page.evaluate(() => window.art.id)).toBe(before)
})
test('stored production and visibility settings reload with their existing keys', async ({
page,
}) => {
await setup(page)
const scripts = []
page.on('request', (request) => {
if (request.resourceType() === 'script')
scripts.push(new URL(request.url()).pathname)
})
await page.addInitScript(() => {
if (!localStorage.getItem('editor-test-init')) {
for (const key of ['prod', 'code', 'log'])
localStorage.setItem(key, 'true')
localStorage.setItem('editor-test-init', 'true')
}
})
await page.goto(`/?code=${encodeURIComponent(player)}`)
await page.waitForFunction(() => window.art?.isReady)
expect(scripts).toContain('/compiled/artplayer.js')
await expect(page.locator('#editor')).toBeHidden()
await expect(page.locator('.console')).toBeHidden()
await page.locator('#code').uncheck()
await expect(page.locator('#editor')).toBeVisible()
await page.waitForFunction(() => window.art?.isReady)
expect(await page.evaluate(() => localStorage.getItem('code'))).toBe('false')
})
test('Monaco language AMD modules finish before example dependencies can suspend define', async ({
page,
}) => {
await setup(page)
let release
let started
const gate = new Promise((resolve) => {
release = resolve
})
const requested = new Promise((resolve) => {
started = resolve
})
let dependencies = 0
await page.route(
'**/vs/basic-languages/typescript/typescript.js',
async (route) => {
const response = await route.fetch()
started()
await gate
await route.fulfill({
response,
body: `window.languageAmdReady = true;\n${await response.text()}`,
})
},
)
await page.route('**/site-editor-dependency.js', (route) => {
dependencies++
return route.fulfill({
contentType: 'text/javascript',
body: 'if (!window.languageAmdReady) throw new Error("Language module was not ready"); window.editorDependencyLoaded = true;',
})
})
await page.goto(
`/?libs=${encodeURIComponent('./site-editor-dependency.js')}&code=${encodeURIComponent(player)}`,
{ waitUntil: 'domcontentloaded' },
)
await requested
await page.locator('.run').dispatchEvent('click')
expect(dependencies).toBe(0)
release()
await page.waitForFunction(
() => window.art?.isReady && window.editorDependencyLoaded,
)
expect(dependencies).toBe(1)
expect(await page.evaluate(() => typeof window.define)).toBe('function')
})
test('failed declaration loading disposes partial models and prevents premature Run', async ({
page,
}) => {
await setup(page)
await page.route('**/assets/ts/artplayer-plugin-chapter.d.ts', route =>
route.fulfill({ status: 404, body: 'Missing declaration' }))
const error = waitError(page, 'Loading editor declaration failed')
await page.goto('/?code=window.prematureRun=true')
await error
await expect(page.locator('.run')).toHaveAttribute('aria-disabled', 'true')
await page.locator('.run').dispatchEvent('click')
expect(
await page.evaluate(() => ({
models: window.monaco.editor.getModels().length,
ran: !!window.prematureRun,
})),
).toEqual({ models: 0, ran: false })
})
+188
View File
@@ -0,0 +1,188 @@
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
// eslint-disable-next-line test/no-import-node-test -- Verify editor failure and race contracts with controlled asynchronous boundaries.
import test from 'node:test'
import vm from 'node:vm'
import ts from 'typescript'
import { readEditorFile } from '../packages/artplayer-vitepress/browser/editor-files.ts'
import {
readPreference,
writePreference,
} from '../packages/artplayer-vitepress/browser/editor-preferences.ts'
import { createEditorSession } from '../packages/artplayer-vitepress/browser/editor-session.ts'
const tick = () => new Promise(resolve => setImmediate(resolve))
function deferred() {
let resolve
const promise = new Promise((done) => {
resolve = done
})
return { promise, resolve }
}
test('old editor executes TS syntax directly and has no file error settlement', async () => {
const old = execFileSync(
'git',
[
'show',
'a468cb2c39e626f7b4f737d0ca60334150deae2b:docs/assets/js/common.js',
],
{ encoding: 'utf8' },
)
const ast = ts.createSourceFile('old.js', old, ts.ScriptTarget.Latest, true)
function find(name) {
let code
function visit(node) {
if (ts.isFunctionDeclaration(node) && node.name.text === name)
code = node.getText(ast)
ts.forEachChild(node, visit)
}
visit(ast)
return code
}
assert.throws(
() =>
vm.runInNewContext(`${find('runCode')};runCode()`, {
Event,
window: { dispatchEvent() {} },
Artplayer: { instances: [] },
editor: { getValue: () => 'const count: number = 7;' },
}),
/Missing initializer|Unexpected token/,
)
let reader
class Reader {
constructor() {
reader = this
}
readAsText() {}
}
const pending = vm.runInNewContext(
`${find('readFile')};readFile({name:'broken.js'})`,
{ FileReader: Reader },
)
let settled = false
pending.then(
() => {
settled = true
},
() => {
settled = true
},
)
assert.equal(reader.onerror, undefined)
await tick()
assert.equal(settled, false)
const html = execFileSync('git', ['show', 'a468cb2c39e626f7b4f737d0ca60334150deae2b:docs/index.html'], { encoding: 'utf8' })
const boot = html.match(/<script>(\s*var artplayer[\s\S]*?)<\/script>/)[1]
assert.throws(() => vm.runInNewContext(boot, {
document: { createElement: () => ({}) },
localStorage: { getItem() { throw new Error('Storage denied') } },
}), /Storage denied/)
})
test('latest editor run wins across delayed example and compiler responses; disposal forbids execution', async () => {
const example = deferred()
const compile = deferred()
const executions = []
const writes = []
const session = createEditorSession({
loadLibraries: async () => {},
exampleSource: () => example.promise,
writeCode: value => writes.push(value),
compile: value =>
value === 'old compile' ? compile.promise : Promise.resolve(value),
execute: code => executions.push(code),
})
const initial = session.run({
example: 'slow',
code: encodeURIComponent('wrong priority'),
})
await tick()
await session.run({ code: encodeURIComponent('new') })
example.resolve('late example')
await initial
assert.deepEqual(writes, ['new'])
const older = session.run({ code: encodeURIComponent('old compile') })
await tick()
await session.run({ code: encodeURIComponent('newest') })
compile.resolve('late compiled')
await older
assert.deepEqual(executions, ['new', 'newest'])
session.dispose()
await session.run({ code: 'never' })
assert.deepEqual(executions, ['new', 'newest'])
})
test('library and compiler failures reject without executing a partial or invalid run', async () => {
for (const where of ['library', 'compile']) {
const session = createEditorSession({
loadLibraries: async () => {
if (where === 'library')
throw new Error('library failed')
},
exampleSource: async () => '',
writeCode() {},
compile: async () => {
throw new Error('compile failed')
},
execute() {
assert.fail('Must not execute')
},
})
await assert.rejects(session.run({ code: 'source' }), /failed/)
}
})
test('file reading settles errors, synchronous failures and cancellation and removes handlers', async () => {
for (const mode of ['error', 'throw', 'abort', 'loaded']) {
const controller = new AbortController()
const reader = {
result: 'source',
error: new Error('unreadable'),
readAsText() {
if (mode === 'throw')
throw new Error('read threw')
if (mode === 'error')
queueMicrotask(() => reader.onerror())
if (mode === 'loaded')
queueMicrotask(() => reader.onload())
},
abort() {
reader.onabort?.()
},
}
const promise = readEditorFile(
{ name: 'input.js' },
controller.signal,
() => reader,
)
if (mode === 'abort')
controller.abort()
if (mode === 'loaded')
assert.equal(await promise, 'source')
else await assert.rejects(promise, /unreadable|threw|cancelled/)
for (const event of ['onload', 'onerror', 'onabort'])
assert.equal(reader[event], null)
}
})
test('preferences retain exact keys and string values and tolerate denied storage', () => {
const values = new Map()
const storage = () => ({
getItem: key => values.get(key),
setItem: (key, value) => values.set(key, value),
})
assert.equal(readPreference('ts', storage), false)
assert.equal(writePreference('ts', true, storage), true)
assert.equal(readPreference('ts', storage), true)
assert.equal(values.get('ts'), 'true')
writePreference('ts', false, storage)
assert.equal(values.get('ts'), 'false')
const denied = () => {
throw new Error('Storage denied')
}
assert.equal(readPreference('prod', denied), false)
assert.equal(writePreference('prod', true, denied), false)
})
+5
View File
@@ -6336,6 +6336,11 @@ modify-values@^1.0.1:
resolved "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz"
integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==
monaco-editor@0.30.1:
version "0.30.1"
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.30.1.tgz#47f8d18a0aa2264fc5654581741ab8d7bec01689"
integrity sha512-B/y4+b2O5G2gjuxIFtCE2EkM17R2NM7/3F8x0qcPsqy4V83bitJTIO4TIeZpYlzu/xy6INiY/+84BEm6+7Cmzg==
ms@^2.1.3:
version "2.1.3"
resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"