mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-10 12:46:15 -08:00
refactor(docs): [SITE-03] migrate desktop editor and integrate typed generation
This commit is contained in:
1 parent
a468cb2c39
commit
2f130c58f1
37 files changed
+1921
-305
No files matched your search
@@ -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
|
||||
|
||||
|
||||
Vendored
+42
@@ -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
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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 模块拆分,暂存后替换并保留失败恢复路径;
|
||||
|
||||
@@ -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 |
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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 使用内置浏览器并注明实际环境。
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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')
|
||||
|
||||
@@ -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`.
|
||||
@@ -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)
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user