mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-14 08:26:16 -08:00
fix(docs): [SITE-LOAD-01] serialize example loading and preserve navigation
This commit is contained in:
1 parent
76499e7a3e
commit
647f3e7f12
29 files changed
+1977
-256
No files matched your search
+39
-109
@@ -1,3 +1,4 @@
|
||||
/* global Artplayer, monaco */
|
||||
(function () {
|
||||
Artplayer.DEBUG = true
|
||||
|
||||
@@ -5,22 +6,22 @@
|
||||
window.location.href = `./mobile.html${location.search}`
|
||||
}
|
||||
|
||||
let $codeMirror = document.querySelector('.codeMirrorWrap')
|
||||
let $lib = document.querySelector('.libsInput')
|
||||
let $run = document.querySelector('.run')
|
||||
let $popups = document.querySelector('.popups')
|
||||
let $console = document.querySelector('.console')
|
||||
let $prod = document.querySelector('#prod')
|
||||
let $ts = document.querySelector('#ts')
|
||||
let $code = document.querySelector('#code')
|
||||
let $log = document.querySelector('#log')
|
||||
let $file = document.querySelector('#file')
|
||||
let $editor = document.querySelector('#editor')
|
||||
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 getStorageBoolean = key => localStorage.getItem(key) === 'true'
|
||||
const setStorageBoolean = (key, value) => localStorage.setItem(key, value ? 'true' : 'false')
|
||||
|
||||
$prod.checked = getStorageBoolean('prod')
|
||||
@@ -37,7 +38,7 @@
|
||||
}
|
||||
|
||||
let editor = null
|
||||
let loadedLibs = []
|
||||
let initialization = 0
|
||||
require.config({ paths: { vs: './assets/js/vs' } })
|
||||
require(['vs/editor/editor.main'], async () => {
|
||||
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
|
||||
@@ -50,7 +51,7 @@
|
||||
allowNonTsExtensions: true,
|
||||
})
|
||||
|
||||
let libUris = [
|
||||
const libUris = [
|
||||
'./assets/ts/artplayer-plugin-ads.d.ts',
|
||||
'./assets/ts/artplayer-plugin-ambilight.d.ts',
|
||||
'./assets/ts/artplayer-plugin-asr.d.ts',
|
||||
@@ -76,8 +77,8 @@
|
||||
]
|
||||
|
||||
for (let index = 0; index < libUris.length; index++) {
|
||||
let libUri = libUris[index]
|
||||
let libSource = await (await fetch(libUri)).text()
|
||||
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))
|
||||
}
|
||||
@@ -108,122 +109,51 @@
|
||||
})
|
||||
})
|
||||
|
||||
function getURLParameters(url) {
|
||||
return (url.match(/([^?=&]+)(=([^&]*))/g) || []).reduce((params, pair) => {
|
||||
const index = pair.indexOf('=')
|
||||
params[pair.slice(0, index)] = pair.slice(index + 1)
|
||||
return params
|
||||
}, {})
|
||||
async function loadLib(libs) {
|
||||
$lib.value = decodeURIComponent(libs || '')
|
||||
return window.ArtplayerDocsLoader.loadLibraries(libs)
|
||||
}
|
||||
|
||||
function getExt(url) {
|
||||
if (url.includes('?')) {
|
||||
return getExt(url.split('?')[0])
|
||||
}
|
||||
|
||||
if (url.includes('#')) {
|
||||
return getExt(url.split('#')[0])
|
||||
}
|
||||
|
||||
return url.trim().toLowerCase().split('.').pop()
|
||||
async function runExample(name, current) {
|
||||
const text = await window.ArtplayerDocsLoader.exampleSource(name)
|
||||
if (current !== initialization)
|
||||
return
|
||||
editor.setValue(text)
|
||||
runCode()
|
||||
}
|
||||
|
||||
function loadScript(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const originalDefine = window.define
|
||||
window.define = undefined
|
||||
const script = document.createElement('script')
|
||||
script.type = 'text/javascript'
|
||||
script.src = url
|
||||
script.onload = () => {
|
||||
window.define = originalDefine
|
||||
resolve(url)
|
||||
}
|
||||
script.onerror = () => {
|
||||
window.define = originalDefine
|
||||
reject(new Error(`Loading script failed: ${url}`))
|
||||
}
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
|
||||
function loadStyle(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const link = document.createElement('link')
|
||||
link.rel = 'stylesheet'
|
||||
link.href = url
|
||||
link.onload = () => resolve(url)
|
||||
link.onerror = () => reject(new Error(`Loading style failed: ${url}`))
|
||||
document.head.appendChild(link)
|
||||
})
|
||||
}
|
||||
|
||||
function loadLib(libs) {
|
||||
const libPromises = []
|
||||
const libsDecoded = decodeURIComponent(libs || '')
|
||||
const urls = libsDecoded
|
||||
.split(/\r?\n/)
|
||||
.filter((url) => url.trim() && !loadedLibs.includes(url))
|
||||
|
||||
urls.forEach((url) => {
|
||||
const ext = getExt(url)
|
||||
if (ext === 'js') {
|
||||
libPromises.push(loadScript(url))
|
||||
} else if (ext === 'css') {
|
||||
libPromises.push(loadStyle(url))
|
||||
}
|
||||
})
|
||||
|
||||
$lib.value = libsDecoded
|
||||
return Promise.all(libPromises)
|
||||
}
|
||||
|
||||
function runExample(name) {
|
||||
fetch(`./assets/example/${name}.js`)
|
||||
.then((response) => {
|
||||
return response.text()
|
||||
})
|
||||
.then((text) => {
|
||||
editor.setValue(text)
|
||||
runCode()
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
function loadCode(code, example) {
|
||||
if (example) {
|
||||
runExample(example)
|
||||
}
|
||||
else if (code) {
|
||||
function loadCode(code, example, current) {
|
||||
if (current !== initialization)
|
||||
return
|
||||
if (example)
|
||||
return runExample(example, current)
|
||||
if (code) {
|
||||
editor.setValue(decodeURIComponent(code).trim())
|
||||
runCode()
|
||||
}
|
||||
else {
|
||||
runExample('index')
|
||||
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))
|
||||
|
||||
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 urlParams = getURLParameters(window.location.href)
|
||||
const current = ++initialization
|
||||
const urlParams = window.ArtplayerDocsLoader.parameters(window.location.href)
|
||||
const { code, libs, example } = urlParams
|
||||
|
||||
loadLib(libs)
|
||||
.then((result) => {
|
||||
loadedLibs.push(...result)
|
||||
loadCode(code, example)
|
||||
})
|
||||
.then(() => loadCode(code, example, current))
|
||||
.catch((err) => {
|
||||
console.error('Failed to initialize app:', err)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
// Generated by yarn build:site-assets. Do not edit.
|
||||
"use strict";
|
||||
(() => {
|
||||
// packages/artplayer-vitepress/browser/loader.ts
|
||||
function parameters(url) {
|
||||
const result = {};
|
||||
for (const part of new URL(url).search.slice(1).split("&")) {
|
||||
const index = part.indexOf("=");
|
||||
if (index >= 0)
|
||||
result[part.slice(0, index)] = part.slice(index + 1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function suspendDefine(host) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(host, "define");
|
||||
if (descriptor?.configurable === false) {
|
||||
if (!("value" in descriptor) || !descriptor.writable)
|
||||
throw new Error("Cannot temporarily suspend the AMD loader");
|
||||
Object.defineProperty(host, "define", { value: void 0 });
|
||||
} else {
|
||||
Object.defineProperty(host, "define", { value: void 0, configurable: true, writable: true, enumerable: descriptor?.enumerable ?? true });
|
||||
}
|
||||
return () => {
|
||||
if (descriptor)
|
||||
Object.defineProperty(host, "define", descriptor);
|
||||
else Reflect.deleteProperty(host, "define");
|
||||
};
|
||||
}
|
||||
function createLibraryLoader(host = window) {
|
||||
const loaded = /* @__PURE__ */ new Set();
|
||||
let queue = Promise.resolve();
|
||||
let cancel;
|
||||
let disposed = false;
|
||||
const ensureActive = () => {
|
||||
if (disposed)
|
||||
throw new Error("Documentation loader was disposed");
|
||||
};
|
||||
function load(url, script) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const element = host.document.createElement(script ? "script" : "link");
|
||||
let restore;
|
||||
let settled = false;
|
||||
function finish(error) {
|
||||
if (settled)
|
||||
return;
|
||||
settled = true;
|
||||
element.removeEventListener("load", success);
|
||||
element.removeEventListener("error", failure);
|
||||
cancel = void 0;
|
||||
try {
|
||||
restore?.();
|
||||
} catch (cause) {
|
||||
error = new Error(`Failed to restore AMD loader: ${String(cause)}`);
|
||||
}
|
||||
if (error) {
|
||||
element.remove();
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
function success() {
|
||||
finish();
|
||||
}
|
||||
function failure() {
|
||||
finish(new Error(`Loading ${script ? "script" : "style"} failed: ${url}`));
|
||||
}
|
||||
element.addEventListener("load", success);
|
||||
element.addEventListener("error", failure);
|
||||
cancel = () => finish(new Error(`Cancelled documentation dependency: ${url}`));
|
||||
try {
|
||||
ensureActive();
|
||||
if (script) {
|
||||
restore = suspendDefine(host);
|
||||
const node = element;
|
||||
node.type = "text/javascript";
|
||||
node.src = url;
|
||||
node.async = false;
|
||||
} else {
|
||||
const node = element;
|
||||
node.rel = "stylesheet";
|
||||
node.href = url;
|
||||
}
|
||||
host.document.head.appendChild(element);
|
||||
} catch (cause) {
|
||||
finish(cause instanceof Error ? cause : new Error(String(cause)));
|
||||
}
|
||||
});
|
||||
}
|
||||
return {
|
||||
loadLibraries(encoded = "") {
|
||||
const work = queue.then(async () => {
|
||||
ensureActive();
|
||||
const urls = decodeURIComponent(encoded).split(/\r?\n/).map((url) => url.trim()).filter(Boolean);
|
||||
const result = [];
|
||||
for (const url of urls) {
|
||||
ensureActive();
|
||||
const absolute = new URL(url, host.location.href);
|
||||
const extension = absolute.pathname.toLowerCase().split(".").pop();
|
||||
if (extension !== "js" && extension !== "css")
|
||||
continue;
|
||||
if (!loaded.has(absolute.href)) {
|
||||
await load(absolute.href, extension === "js");
|
||||
loaded.add(absolute.href);
|
||||
}
|
||||
result.push(url);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
queue = work.catch(() => {
|
||||
});
|
||||
return work;
|
||||
},
|
||||
dispose() {
|
||||
disposed = true;
|
||||
cancel?.();
|
||||
loaded.clear();
|
||||
}
|
||||
};
|
||||
}
|
||||
async function exampleSource(name, request = fetch) {
|
||||
const response = await request(`./assets/example/${name}.js`);
|
||||
if (!response.ok)
|
||||
throw new Error(`Loading example failed: ${name} (${response.status})`);
|
||||
return response.text();
|
||||
}
|
||||
|
||||
// packages/artplayer-vitepress/browser/editor-loader.ts
|
||||
var loader = createLibraryLoader();
|
||||
Object.assign(window, { ArtplayerDocsLoader: { ...loader, exampleSource, parameters } });
|
||||
window.addEventListener("pagehide", (event) => {
|
||||
if (!event.persisted)
|
||||
loader.dispose();
|
||||
});
|
||||
})();
|
||||
+137
-95
@@ -1,100 +1,142 @@
|
||||
(function () {
|
||||
Artplayer.DEBUG = true;
|
||||
|
||||
function getURLParameters(url) {
|
||||
return (url.match(/([^?=&]+)(=([^&]*))/g) || []).reduce(function (a, v) {
|
||||
return (a[v.slice(0, v.indexOf('='))] = v.slice(v.indexOf('=') + 1)), a;
|
||||
}, {});
|
||||
// Generated by yarn build:site-assets. Do not edit.
|
||||
"use strict";
|
||||
(() => {
|
||||
// packages/artplayer-vitepress/browser/loader.ts
|
||||
function parameters(url) {
|
||||
const result = {};
|
||||
for (const part of new URL(url).search.slice(1).split("&")) {
|
||||
const index = part.indexOf("=");
|
||||
if (index >= 0)
|
||||
result[part.slice(0, index)] = part.slice(index + 1);
|
||||
}
|
||||
|
||||
function getExt(url) {
|
||||
if (url.includes('?')) {
|
||||
return getExt(url.split('?')[0]);
|
||||
}
|
||||
|
||||
if (url.includes('#')) {
|
||||
return getExt(url.split('#')[0]);
|
||||
}
|
||||
|
||||
return url.trim().toLowerCase().split('.').pop();
|
||||
return result;
|
||||
}
|
||||
function suspendDefine(host) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(host, "define");
|
||||
if (descriptor?.configurable === false) {
|
||||
if (!("value" in descriptor) || !descriptor.writable)
|
||||
throw new Error("Cannot temporarily suspend the AMD loader");
|
||||
Object.defineProperty(host, "define", { value: void 0 });
|
||||
} else {
|
||||
Object.defineProperty(host, "define", { value: void 0, configurable: true, writable: true, enumerable: descriptor?.enumerable ?? true });
|
||||
}
|
||||
|
||||
function loadScript(url) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var define2 = window.define;
|
||||
window.define = undefined;
|
||||
var script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.src = url;
|
||||
script.onload = function () {
|
||||
window.define = define2;
|
||||
resolve(url);
|
||||
};
|
||||
script.onerror = function () {
|
||||
reject(new Error('Loading script failed:' + url));
|
||||
};
|
||||
document.querySelector('head').appendChild(script);
|
||||
return () => {
|
||||
if (descriptor)
|
||||
Object.defineProperty(host, "define", descriptor);
|
||||
else Reflect.deleteProperty(host, "define");
|
||||
};
|
||||
}
|
||||
function createLibraryLoader(host = window) {
|
||||
const loaded = /* @__PURE__ */ new Set();
|
||||
let queue = Promise.resolve();
|
||||
let cancel;
|
||||
let disposed = false;
|
||||
const ensureActive = () => {
|
||||
if (disposed)
|
||||
throw new Error("Documentation loader was disposed");
|
||||
};
|
||||
function load(url, script) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const element = host.document.createElement(script ? "script" : "link");
|
||||
let restore;
|
||||
let settled = false;
|
||||
function finish(error) {
|
||||
if (settled)
|
||||
return;
|
||||
settled = true;
|
||||
element.removeEventListener("load", success);
|
||||
element.removeEventListener("error", failure);
|
||||
cancel = void 0;
|
||||
try {
|
||||
restore?.();
|
||||
} catch (cause) {
|
||||
error = new Error(`Failed to restore AMD loader: ${String(cause)}`);
|
||||
}
|
||||
if (error) {
|
||||
element.remove();
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
function success() {
|
||||
finish();
|
||||
}
|
||||
function failure() {
|
||||
finish(new Error(`Loading ${script ? "script" : "style"} failed: ${url}`));
|
||||
}
|
||||
element.addEventListener("load", success);
|
||||
element.addEventListener("error", failure);
|
||||
cancel = () => finish(new Error(`Cancelled documentation dependency: ${url}`));
|
||||
try {
|
||||
ensureActive();
|
||||
if (script) {
|
||||
restore = suspendDefine(host);
|
||||
const node = element;
|
||||
node.type = "text/javascript";
|
||||
node.src = url;
|
||||
node.async = false;
|
||||
} else {
|
||||
const node = element;
|
||||
node.rel = "stylesheet";
|
||||
node.href = url;
|
||||
}
|
||||
host.document.head.appendChild(element);
|
||||
} catch (cause) {
|
||||
finish(cause instanceof Error ? cause : new Error(String(cause)));
|
||||
}
|
||||
});
|
||||
}
|
||||
return {
|
||||
loadLibraries(encoded = "") {
|
||||
const work = queue.then(async () => {
|
||||
ensureActive();
|
||||
const urls = decodeURIComponent(encoded).split(/\r?\n/).map((url) => url.trim()).filter(Boolean);
|
||||
const result = [];
|
||||
for (const url of urls) {
|
||||
ensureActive();
|
||||
const absolute = new URL(url, host.location.href);
|
||||
const extension = absolute.pathname.toLowerCase().split(".").pop();
|
||||
if (extension !== "js" && extension !== "css")
|
||||
continue;
|
||||
if (!loaded.has(absolute.href)) {
|
||||
await load(absolute.href, extension === "js");
|
||||
loaded.add(absolute.href);
|
||||
}
|
||||
result.push(url);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
function loadStyle(url) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = url;
|
||||
link.onload = function () {
|
||||
resolve(url);
|
||||
};
|
||||
link.onerror = function () {
|
||||
reject(new Error('Loading style failed:' + url));
|
||||
};
|
||||
document.querySelector('head').appendChild(link);
|
||||
queue = work.catch(() => {
|
||||
});
|
||||
}
|
||||
return work;
|
||||
},
|
||||
dispose() {
|
||||
disposed = true;
|
||||
cancel?.();
|
||||
loaded.clear();
|
||||
}
|
||||
};
|
||||
}
|
||||
async function exampleSource(name, request = fetch) {
|
||||
const response = await request(`./assets/example/${name}.js`);
|
||||
if (!response.ok)
|
||||
throw new Error(`Loading example failed: ${name} (${response.status})`);
|
||||
return response.text();
|
||||
}
|
||||
|
||||
function loadLib(libs) {
|
||||
var libPromise = [];
|
||||
var libsDecode = decodeURIComponent(libs || '');
|
||||
libsDecode
|
||||
.split(/\r?\n/)
|
||||
.forEach(function (url) {
|
||||
var ext = getExt(url);
|
||||
if (ext === 'js') {
|
||||
libPromise.push(loadScript(url));
|
||||
} else if (ext === 'css') {
|
||||
libPromise.push(loadStyle(url));
|
||||
}
|
||||
});
|
||||
return Promise.all(libPromise);
|
||||
}
|
||||
|
||||
function runExample(name) {
|
||||
fetch('./assets/example/' + name + '.js')
|
||||
.then(function (response) {
|
||||
return response.text();
|
||||
})
|
||||
.then(function (text) {
|
||||
eval(text);
|
||||
})
|
||||
}
|
||||
|
||||
function loadCode(code, example) {
|
||||
if (example) {
|
||||
runExample(example);
|
||||
} else if (code) {
|
||||
eval(decodeURIComponent(code).trim());
|
||||
} else {
|
||||
runExample('mobile');
|
||||
}
|
||||
}
|
||||
|
||||
var _getURLParameters = getURLParameters(window.location.href),
|
||||
code = _getURLParameters.code,
|
||||
libs = _getURLParameters.libs;
|
||||
example = _getURLParameters.example;
|
||||
|
||||
loadLib(libs)
|
||||
.then(function () {
|
||||
loadCode(code, example);
|
||||
})
|
||||
})();
|
||||
// packages/artplayer-vitepress/browser/mobile.ts
|
||||
window.Artplayer.DEBUG = true;
|
||||
var loader = createLibraryLoader();
|
||||
window.addEventListener("pagehide", (event) => {
|
||||
if (!event.persisted)
|
||||
loader.dispose();
|
||||
});
|
||||
async function start() {
|
||||
const { code, libs, example } = parameters(location.href);
|
||||
await loader.loadLibraries(libs);
|
||||
const source = example ? await exampleSource(example) : code ? decodeURIComponent(code).trim() : await exampleSource("mobile");
|
||||
new Function(source)();
|
||||
}
|
||||
start().catch((error) => console.error("Failed to initialize mobile example:", error));
|
||||
})();
|
||||
@@ -229,6 +229,7 @@
|
||||
<div class="popinner"></div>
|
||||
</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');
|
||||
|
||||
+9
-6
@@ -30,7 +30,7 @@
|
||||
"postinstall": "npx lerna run prepare",
|
||||
"build:ts": "node ./scripts/build-ts.js",
|
||||
"build:llm": "node ./scripts/build-llm.js",
|
||||
"build:docs": "node ./scripts/build-docs.js",
|
||||
"build:docs": "yarn build:site-assets && node ./scripts/build-docs.js",
|
||||
"build:i18n": "node ./scripts/build-i18n.js",
|
||||
"build:test": "node ./scripts/build-test.js",
|
||||
"test:playback": "node --test test/playback.test.js",
|
||||
@@ -40,14 +40,14 @@
|
||||
"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}\" \"scripts/{docs-smoke,editor-declarations}/**/*.ts\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" \"playwright*.config.js\" --no-fix",
|
||||
"lint": "eslint \"packages/*/{src,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"packages/artplayer-vitepress/browser/**/*.ts\" \"scripts/{docs-smoke,editor-declarations}/**/*.ts\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" \"playwright*.config.js\" --no-fix docs/assets/js/common.js",
|
||||
"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}\" \"scripts/{docs-smoke,editor-declarations}/**/*.ts\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" \"playwright*.config.js\" --fix",
|
||||
"lint:fix": "eslint \"packages/*/{src,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"packages/artplayer-vitepress/browser/**/*.ts\" \"scripts/{docs-smoke,editor-declarations}/**/*.ts\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" \"playwright*.config.js\" --fix docs/assets/js/common.js",
|
||||
"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: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: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:types && yarn typecheck && yarn typecheck:react && yarn lint:react && yarn typecheck:vue && yarn lint:vue && yarn test",
|
||||
"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 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:test && yarn build:docs && yarn test:imports",
|
||||
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js test/asr-distribution.test.js",
|
||||
"typecheck": "node scripts/typecheck.mjs",
|
||||
@@ -121,7 +121,10 @@
|
||||
"lint:vue": "eslint example/vue.js --no-fix",
|
||||
"typecheck:docs-tools": "node node_modules/typescript/bin/tsc -p scripts/tsconfig.docs.json --noEmit",
|
||||
"check:docs-smoke": "node scripts/build-test.js --check",
|
||||
"check:editor-types": "node scripts/build-ts.js --check"
|
||||
"check:editor-types": "node scripts/build-ts.js --check",
|
||||
"build:site-assets": "node scripts/build-site-assets.mjs",
|
||||
"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"
|
||||
},
|
||||
"browserslist": "last 1 Chrome version",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -22,6 +22,12 @@ and 13 English. Danmuku has no English counterpart yet. Other plugins currently
|
||||
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
|
||||
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.
|
||||
|
||||
## Related tooling
|
||||
|
||||
Run commands from the repository root with its pinned Node version and Yarn
|
||||
@@ -69,12 +75,12 @@ have separate dependencies. See the repository
|
||||
[site inventory](../../refactor/site-inventory.md) for the full entrypoint map,
|
||||
source fingerprints, asset provenance and assigned follow-up work.
|
||||
|
||||
`main.js` installs one document click listener under `window['run-code-init']`;
|
||||
its lifetime is the page. Run Code opens the separate editor. The development
|
||||
hostname check is currently exactly `localhost`; `127.0.0.1` follows the remote
|
||||
link path. The language redirect writes `lang-init` and can navigate to the
|
||||
English root on a first non-Chinese visit. SITE-03/SITE-05 must test these paths
|
||||
before modifying routing or VitePress client lifecycle behavior.
|
||||
Generated `main.js` installs one document click listener under
|
||||
`window['run-code-init']`; its lifetime is the page. Run Code opens the separate
|
||||
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.
|
||||
|
||||
## Delivery and remaining acceptance
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# Documentation browser modules
|
||||
|
||||
These are site-owned TypeScript sources, not player package APIs. Use the pinned
|
||||
Node 24.21.0 / Yarn 1.22.22 toolchain from the repository root.
|
||||
|
||||
| Module | Ownership |
|
||||
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `loader.ts` | Raw URL parameters, serialized script/style loading, successful URL cache, AMD restoration and HTTP example-source checks |
|
||||
| `editor-loader.ts` | One desktop loader exposed as the private site bridge `window.ArtplayerDocsLoader` |
|
||||
| `mobile.ts` | Mobile DEBUG setup, dependency completion, example/code/default priority and classic-script evaluation |
|
||||
| `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
|
||||
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.
|
||||
|
||||
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
|
||||
fails. Failed URLs are removed and may be retried. Query strings remain part of
|
||||
cache identity; unknown file extensions are ignored as before. CSS follows the
|
||||
same input order but never changes AMD state. There is no new network timeout.
|
||||
|
||||
A script temporarily masks the own `window.define` descriptor and restores the
|
||||
exact descriptor, or its absence, on success, error, append failure or cancellation.
|
||||
An immutable incompatible descriptor is rejected before insertion. Third-party
|
||||
code that irreversibly changes that descriptor can prevent restoration; the error
|
||||
is reported rather than treating the dependency as loaded. The loader does not
|
||||
roll back arbitrary global side effects of code that already executed.
|
||||
|
||||
`dispose()` rejects pending loads, removes their listeners/elements and rejects
|
||||
future calls; loaded script/style nodes remain page-owned. Removing a classic
|
||||
script does not guarantee that the browser cancels already-started evaluation.
|
||||
Non-persisted pagehide disposes the loader. BFCache
|
||||
pagehide leaves the page's loader usable on restoration. Physical-device/BFCache
|
||||
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.
|
||||
|
||||
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
|
||||
errors are reported before evaluating response bodies. Mobile evaluates in a fresh
|
||||
classic function scope, preserving ordinary `var` and non-strict example behavior.
|
||||
This editor intentionally runs user-selected code and is not a security sandbox.
|
||||
|
||||
Run Code encodes both libraries and code once, supports clicks on nested children,
|
||||
and safely ignores missing code siblings. localhost, 127.0.0.1 and IPv6 loopback
|
||||
target their local HTTP port 8082; production retains the canonical editor URL.
|
||||
First non-Chinese visits keep matching English deep links, query and fragment.
|
||||
Pages without an English counterpart fall back to the English index. Already-English
|
||||
visits record the existing lang-init key without a redundant reload; later explicit
|
||||
Chinese selections remain. Blocked storage keeps the requested page, avoiding loops.
|
||||
The available counterparts are derived from current English Markdown during build.
|
||||
|
||||
```sh
|
||||
yarn build:site-assets
|
||||
yarn check:site-assets
|
||||
yarn typecheck:site-assets
|
||||
yarn typecheck:docs-tools
|
||||
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
|
||||
commands still require regenerating these assets after source edits. The full
|
||||
VitePress build/deploy, i18n and remote translation/LLM tooling remain SITE-03/05.
|
||||
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
|
||||
resource contention is observed; record infrastructure failures, never hide them
|
||||
by increasing timeouts or silently retrying.
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createLibraryLoader, exampleSource, parameters } from './loader.ts'
|
||||
|
||||
const loader = createLibraryLoader()
|
||||
Object.assign(window, { ArtplayerDocsLoader: { ...loader, exampleSource, parameters } })
|
||||
window.addEventListener('pagehide', (event) => {
|
||||
if (!event.persisted)
|
||||
loader.dispose()
|
||||
})
|
||||
@@ -0,0 +1,129 @@
|
||||
export function parameters(url: string): Record<string, string> {
|
||||
const result: Record<string, string> = {}
|
||||
for (const part of new URL(url).search.slice(1).split('&')) {
|
||||
const index = part.indexOf('=')
|
||||
if (index >= 0)
|
||||
result[part.slice(0, index)] = part.slice(index + 1)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function suspendDefine(host: Window): () => void {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(host, 'define')
|
||||
if (descriptor?.configurable === false) {
|
||||
if (!('value' in descriptor) || !descriptor.writable)
|
||||
throw new Error('Cannot temporarily suspend the AMD loader')
|
||||
Object.defineProperty(host, 'define', { value: undefined })
|
||||
}
|
||||
else {
|
||||
Object.defineProperty(host, 'define', { value: undefined, configurable: true, writable: true, enumerable: descriptor?.enumerable ?? true })
|
||||
}
|
||||
return () => {
|
||||
if (descriptor)
|
||||
Object.defineProperty(host, 'define', descriptor)
|
||||
else Reflect.deleteProperty(host, 'define')
|
||||
}
|
||||
}
|
||||
|
||||
export function createLibraryLoader(host: Window = window) {
|
||||
const loaded = new Set<string>()
|
||||
let queue: Promise<unknown> = Promise.resolve()
|
||||
let cancel: (() => void) | undefined
|
||||
let disposed = false
|
||||
const ensureActive = () => {
|
||||
if (disposed)
|
||||
throw new Error('Documentation loader was disposed')
|
||||
}
|
||||
function load(url: string, script: boolean): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const element = host.document.createElement(script ? 'script' : 'link')
|
||||
let restore: (() => void) | undefined
|
||||
let settled = false
|
||||
function finish(error?: Error) {
|
||||
if (settled)
|
||||
return
|
||||
settled = true
|
||||
element.removeEventListener('load', success)
|
||||
element.removeEventListener('error', failure)
|
||||
cancel = undefined
|
||||
try {
|
||||
restore?.()
|
||||
}
|
||||
catch (cause) {
|
||||
error = new Error(`Failed to restore AMD loader: ${String(cause)}`)
|
||||
}
|
||||
if (error) {
|
||||
element.remove()
|
||||
reject(error)
|
||||
}
|
||||
else {
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
function success() {
|
||||
finish()
|
||||
}
|
||||
function failure() {
|
||||
finish(new Error(`Loading ${script ? 'script' : 'style'} failed: ${url}`))
|
||||
}
|
||||
element.addEventListener('load', success)
|
||||
element.addEventListener('error', failure)
|
||||
cancel = () => finish(new Error(`Cancelled documentation dependency: ${url}`))
|
||||
try {
|
||||
ensureActive()
|
||||
if (script) {
|
||||
restore = suspendDefine(host)
|
||||
const node = element as HTMLScriptElement
|
||||
node.type = 'text/javascript'
|
||||
node.src = url
|
||||
node.async = false
|
||||
}
|
||||
else {
|
||||
const node = element as HTMLLinkElement
|
||||
node.rel = 'stylesheet'
|
||||
node.href = url
|
||||
}
|
||||
host.document.head.appendChild(element)
|
||||
}
|
||||
catch (cause) {
|
||||
finish(cause instanceof Error ? cause : new Error(String(cause)))
|
||||
}
|
||||
})
|
||||
}
|
||||
return {
|
||||
loadLibraries(encoded: string = ''): Promise<string[]> {
|
||||
const work = queue.then(async () => {
|
||||
ensureActive()
|
||||
const urls = decodeURIComponent(encoded).split(/\r?\n/).map(url => url.trim()).filter(Boolean)
|
||||
const result: string[] = []
|
||||
for (const url of urls) {
|
||||
ensureActive()
|
||||
const absolute = new URL(url, host.location.href)
|
||||
const extension = absolute.pathname.toLowerCase().split('.').pop()
|
||||
if (extension !== 'js' && extension !== 'css')
|
||||
continue
|
||||
if (!loaded.has(absolute.href)) {
|
||||
await load(absolute.href, extension === 'js')
|
||||
loaded.add(absolute.href)
|
||||
}
|
||||
result.push(url)
|
||||
}
|
||||
return result
|
||||
})
|
||||
queue = work.catch(() => {})
|
||||
return work
|
||||
},
|
||||
dispose(): void {
|
||||
disposed = true
|
||||
cancel?.()
|
||||
loaded.clear()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export async function exampleSource(name: string, request: typeof fetch = fetch): Promise<string> {
|
||||
const response = await request(`./assets/example/${name}.js`)
|
||||
if (!response.ok)
|
||||
throw new Error(`Loading example failed: ${name} (${response.status})`)
|
||||
return response.text()
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type Artplayer from '../../artplayer/public/artplayer.ts'
|
||||
import { createLibraryLoader, exampleSource, parameters } from './loader.ts'
|
||||
|
||||
declare global {
|
||||
interface Window { Artplayer: typeof Artplayer }
|
||||
}
|
||||
|
||||
window.Artplayer.DEBUG = true
|
||||
const loader = createLibraryLoader()
|
||||
window.addEventListener('pagehide', (event) => {
|
||||
if (!event.persisted)
|
||||
loader.dispose()
|
||||
})
|
||||
|
||||
async function start(): Promise<void> {
|
||||
const { code, libs, example } = parameters(location.href)
|
||||
await loader.loadLibraries(libs)
|
||||
const source = example ? await exampleSource(example) : code ? decodeURIComponent(code).trim() : await exampleSource('mobile')
|
||||
// eslint-disable-next-line no-new-func -- Run user-selected example code in the existing classic-script function scope.
|
||||
new Function(source)()
|
||||
}
|
||||
|
||||
start().catch(error => console.error('Failed to initialize mobile example:', error))
|
||||
@@ -0,0 +1,53 @@
|
||||
export function isDevelopment(hostname: string): boolean {
|
||||
return ['localhost', '127.0.0.1', '[::1]', '::1'].includes(hostname.toLowerCase())
|
||||
}
|
||||
|
||||
export function runCodeUrl(location: Pick<Location, 'hostname'>, libraries: string, code: string): string {
|
||||
const hostname = location.hostname === '::1' ? '[::1]' : location.hostname
|
||||
const origin = isDevelopment(hostname) ? `http://${hostname}:8082` : 'https://artplayer.org'
|
||||
return `${origin}/?libs=${encodeURIComponent(libraries)}&code=${encodeURIComponent(code)}`
|
||||
}
|
||||
|
||||
export function languageDestination(url: string, language: string, initialized: boolean, englishPaths: readonly string[]): { remember: boolean, target: string | null } {
|
||||
const current = new URL(url)
|
||||
if (initialized || isDevelopment(current.hostname) || language.toLowerCase().startsWith('zh') || !language || !current.pathname.startsWith('/document/'))
|
||||
return { remember: false, target: null }
|
||||
if (current.pathname.startsWith('/document/en/'))
|
||||
return { remember: true, target: null }
|
||||
const relative = current.pathname.slice('/document/'.length)
|
||||
const counterpart = englishPaths.includes(relative)
|
||||
const target = new URL(counterpart ? `/document/en/${relative}` : '/document/en/', current.origin)
|
||||
if (counterpart) {
|
||||
target.search = current.search
|
||||
target.hash = current.hash
|
||||
}
|
||||
return { remember: true, target: target.href }
|
||||
}
|
||||
|
||||
export function installNavigation(host: Window, englishPaths: readonly string[]): void {
|
||||
const marker = 'run-code-init'
|
||||
if (Reflect.get(host, marker))
|
||||
return
|
||||
Reflect.set(host, marker, true)
|
||||
host.document.addEventListener('click', (event) => {
|
||||
const element = event.target instanceof Element ? event.target : null
|
||||
const button = element?.closest('.run-code, [classname="run-code"]')
|
||||
const code = button?.nextElementSibling?.querySelector<HTMLElement>('pre code')
|
||||
if (!(button instanceof HTMLElement) || !code)
|
||||
return
|
||||
event.preventDefault()
|
||||
// eslint-disable-next-line unicorn/prefer-dom-node-text-content -- Preserve the existing rendered Run Code text, including visible line breaks.
|
||||
const text = code.innerText ?? code.textContent ?? ''
|
||||
host.open(runCodeUrl(host.location, button.dataset.libs || '', text))
|
||||
})
|
||||
try {
|
||||
const result = languageDestination(host.location.href, host.navigator.language, !!host.localStorage.getItem('lang-init'), englishPaths)
|
||||
if (result.remember)
|
||||
host.localStorage.setItem('lang-init', 'true')
|
||||
if (result.target)
|
||||
host.location.href = result.target
|
||||
}
|
||||
catch {
|
||||
// Storage may be unavailable. Keep the requested page instead of risking a redirect loop.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": { "allowImportingTsExtensions": true },
|
||||
"include": ["*.ts"]
|
||||
}
|
||||
@@ -1,27 +1,54 @@
|
||||
(function () {
|
||||
if (window['run-code-init']) return;
|
||||
window['run-code-init'] = true;
|
||||
document.addEventListener('click', (event) => {
|
||||
if (event.target.className === 'run-code' || event.target.getAttribute('classname') === 'run-code') {
|
||||
event.preventDefault();
|
||||
const codeElement = event.target.nextElementSibling.querySelector('pre code');
|
||||
if (codeElement) {
|
||||
const libs = event.target.dataset.libs || '';
|
||||
const code = encodeURIComponent(codeElement.innerText);
|
||||
const isDev = location.hostname === 'localhost';
|
||||
const env = isDev ? 'http://localhost:8082' : 'https://artplayer.org';
|
||||
const url = env + '/?libs=' + libs + '&code=' + code;
|
||||
window.open(url);
|
||||
}
|
||||
}
|
||||
// Generated by yarn build:site-assets. Do not edit.
|
||||
(() => {
|
||||
// packages/artplayer-vitepress/browser/navigation.ts
|
||||
function isDevelopment(hostname) {
|
||||
return ["localhost", "127.0.0.1", "[::1]", "::1"].includes(hostname.toLowerCase());
|
||||
}
|
||||
function runCodeUrl(location, libraries, code) {
|
||||
const hostname = location.hostname === "::1" ? "[::1]" : location.hostname;
|
||||
const origin = isDevelopment(hostname) ? `http://${hostname}:8082` : "https://artplayer.org";
|
||||
return `${origin}/?libs=${encodeURIComponent(libraries)}&code=${encodeURIComponent(code)}`;
|
||||
}
|
||||
function languageDestination(url, language, initialized, englishPaths) {
|
||||
const current = new URL(url);
|
||||
if (initialized || isDevelopment(current.hostname) || language.toLowerCase().startsWith("zh") || !language || !current.pathname.startsWith("/document/"))
|
||||
return { remember: false, target: null };
|
||||
if (current.pathname.startsWith("/document/en/"))
|
||||
return { remember: true, target: null };
|
||||
const relative = current.pathname.slice("/document/".length);
|
||||
const counterpart = englishPaths.includes(relative);
|
||||
const target = new URL(counterpart ? `/document/en/${relative}` : "/document/en/", current.origin);
|
||||
if (counterpart) {
|
||||
target.search = current.search;
|
||||
target.hash = current.hash;
|
||||
}
|
||||
return { remember: true, target: target.href };
|
||||
}
|
||||
function installNavigation(host, englishPaths) {
|
||||
const marker = "run-code-init";
|
||||
if (Reflect.get(host, marker))
|
||||
return;
|
||||
Reflect.set(host, marker, true);
|
||||
host.document.addEventListener("click", (event) => {
|
||||
const element = event.target instanceof Element ? event.target : null;
|
||||
const button = element?.closest('.run-code, [classname="run-code"]');
|
||||
const code = button?.nextElementSibling?.querySelector("pre code");
|
||||
if (!(button instanceof HTMLElement) || !code)
|
||||
return;
|
||||
event.preventDefault();
|
||||
const text = code.innerText ?? code.textContent ?? "";
|
||||
host.open(runCodeUrl(host.location, button.dataset.libs || "", text));
|
||||
});
|
||||
})();
|
||||
try {
|
||||
const result = languageDestination(host.location.href, host.navigator.language, !!host.localStorage.getItem("lang-init"), englishPaths);
|
||||
if (result.remember)
|
||||
host.localStorage.setItem("lang-init", "true");
|
||||
if (result.target)
|
||||
host.location.href = result.target;
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
(function () {
|
||||
const language = window.navigator?.language || '';
|
||||
const isDev = location.hostname === 'localhost';
|
||||
if (!language.startsWith('zh') && !localStorage.getItem('lang-init') && !isDev) {
|
||||
localStorage.setItem('lang-init', 'true');
|
||||
window.location.href = 'https://artplayer.org/document/en/';
|
||||
}
|
||||
})();
|
||||
// <stdin>
|
||||
installNavigation(window, ["advanced/built-in.html", "advanced/built-in", "advanced/class.html", "advanced/class", "advanced/event.html", "advanced/event", "advanced/global.html", "advanced/global", "advanced/plugin.html", "advanced/plugin", "advanced/property.html", "advanced/property", "component/contextmenu.html", "component/contextmenu", "component/controls.html", "component/controls", "component/layers.html", "component/layers", "component/setting.html", "component/setting", "", "index.html", "start/i18n.html", "start/i18n", "start/option.html", "start/option"]);
|
||||
})();
|
||||
@@ -17913,11 +17913,12 @@
|
||||
{
|
||||
"source": "docs/index.html",
|
||||
"route": "/",
|
||||
"sha256Lf": "5285062f253dbf739050b00090b65296ffa1078594bd49255d315bd3ec3bdedc",
|
||||
"sha256Lf": "f654fa04c8aea08bc718e96ce2a438f04cc91060b2bb9b2b86f4e1467258fd97",
|
||||
"kind": "demo-or-editor",
|
||||
"scriptSources": [
|
||||
"https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-8579453810848662",
|
||||
"./assets/js/console.js"
|
||||
"./assets/js/console.js",
|
||||
"./assets/js/loader.js"
|
||||
],
|
||||
"status": "not-run",
|
||||
"owner": "EX-03"
|
||||
@@ -18137,15 +18138,15 @@
|
||||
},
|
||||
{
|
||||
"file": "docs/assets/js/common.js",
|
||||
"sha256Lf": "cc9f7d780cf4f0ffb9aaee75da69e2b2b062f3bac07b4ffc9a75956b2e7cd7af"
|
||||
"sha256Lf": "c18f72dd5489bdee5b7bd0030c716eab08ac7ea7bd253353eaf406fb8ae2bc14"
|
||||
},
|
||||
{
|
||||
"file": "docs/assets/js/mobile.js",
|
||||
"sha256Lf": "fd793b52d566a0ba94d4f2ccf85009d8f79d313aaeeeadad5cbb9cd8edd2e171"
|
||||
"sha256Lf": "53a03d1c6725942ee306d2d38fb7ee31d756f16d68f7e0e38f0f01d6073dbf47"
|
||||
},
|
||||
{
|
||||
"file": "packages/artplayer-vitepress/docs/public/main.js",
|
||||
"sha256Lf": "f966a945ecc22e1521208eeb57fe6402f61a2a94f85b0cbb9784ddb1912284fc"
|
||||
"sha256Lf": "b8c9b9334abc800e12fd2b8154d89f04c580adf0a155718f2ed44e85f7fd44d7"
|
||||
},
|
||||
{
|
||||
"file": "packages/artplayer-vitepress/docs/vite.config.ts",
|
||||
@@ -18155,6 +18156,10 @@
|
||||
"file": "packages/artplayer-vitepress/docs/.vitepress/config.js",
|
||||
"sha256Lf": "9b2741347afb49d830dafa2a847625e6b1d6a820e930fb119c91b22dbbcc52d1"
|
||||
},
|
||||
{
|
||||
"file": "scripts/build-site-assets.mjs",
|
||||
"sha256Lf": "58c163e16efa901d227045b468f324ac79ad4ab2f8ab8716f10a8ede62091666"
|
||||
},
|
||||
{
|
||||
"file": "scripts/docs-smoke/generator.ts",
|
||||
"sha256Lf": "2ae4144f27b3050869217e437ddca75ccc25897bf78f75efc91dd1f7d2ca3dc2"
|
||||
@@ -18194,12 +18199,28 @@
|
||||
{
|
||||
"file": "scripts/editor-declarations/vast-sdk.ts",
|
||||
"sha256Lf": "6da9176284b7a697a922b90befa64263e016f12eef483793cc524bdfc028ad05"
|
||||
},
|
||||
{
|
||||
"file": "packages/artplayer-vitepress/browser/editor-loader.ts",
|
||||
"sha256Lf": "7cfdc82726d164d450b21680d950ec3f508482f1ab743febee473855248568e1"
|
||||
},
|
||||
{
|
||||
"file": "packages/artplayer-vitepress/browser/loader.ts",
|
||||
"sha256Lf": "d5d5c079854f40025be6af457baba863ab8cd37af59c120e811b7f59e5c160b6"
|
||||
},
|
||||
{
|
||||
"file": "packages/artplayer-vitepress/browser/mobile.ts",
|
||||
"sha256Lf": "beba095b7fda9fb9aa2c5cd50437ea9e186fa76e700e6e79ee8e9eb67fd86d7d"
|
||||
},
|
||||
{
|
||||
"file": "packages/artplayer-vitepress/browser/navigation.ts",
|
||||
"sha256Lf": "1ad87a6bee717f9e62d28c5e6dc382c5c1b43c09c9f10c64e3ef0824f2fbdedc"
|
||||
}
|
||||
],
|
||||
"generationCommands": {
|
||||
"build:ts": "node ./scripts/build-ts.js",
|
||||
"build:llm": "node ./scripts/build-llm.js",
|
||||
"build:docs": "node ./scripts/build-docs.js",
|
||||
"build:docs": "yarn build:site-assets && node ./scripts/build-docs.js",
|
||||
"build:i18n": "node ./scripts/build-i18n.js",
|
||||
"build:test": "node ./scripts/build-test.js",
|
||||
"build:all": "yarn ci:build && yarn lint",
|
||||
|
||||
@@ -0,0 +1,521 @@
|
||||
{
|
||||
"task": "SITE-LOAD-01",
|
||||
"baseline": "76499e7a3e033c8350ff495ba8bfff4ca5c08d25",
|
||||
"node": "24.21.0",
|
||||
"yarn": "1.22.22",
|
||||
"scope": {
|
||||
"generatedAssets": 3,
|
||||
"englishRouteVariants": 26,
|
||||
"newNodeTests": 5,
|
||||
"newBrowserTests": 21,
|
||||
"monacoRegressionTests": 3,
|
||||
"rootLintExistingWarnings": 1,
|
||||
"publicPackageRuntimeChanged": false,
|
||||
"publicPackageDeclarationsChanged": false,
|
||||
"lockUnchanged": true
|
||||
},
|
||||
"logs": {
|
||||
"newNodeTests": {
|
||||
"file": "refactor/.cache/site-loading-tests.log",
|
||||
"sha256": "ca44177f795a8658e55e6e25a40c426a3625031f5af25c73e7596baf06034806",
|
||||
"exitCode": 0,
|
||||
"pass": 5
|
||||
},
|
||||
"finalTargeted": {
|
||||
"file": "refactor/.cache/site-loading-targeted-final.log",
|
||||
"sha256": "38f02e6e36a08816f006600f79334b98e2bf46b1aba3dca47583a0c8ba7a5a8f",
|
||||
"exitCode": 0,
|
||||
"pass": 13
|
||||
},
|
||||
"baseline": {
|
||||
"file": "refactor/.cache/site-loading-baseline.log",
|
||||
"sha256": "9750bf8d1100cf565b4873d6d36f63a07e48aa39941283ce4bcfa0bd0ff6d9b9",
|
||||
"exitCode": 0,
|
||||
"pass": 522
|
||||
},
|
||||
"browserInitial": {
|
||||
"file": "refactor/.cache/site-loading-browser.log",
|
||||
"sha256": "d02cca26fd1443c5a02f580bf44a0816dd39e28eb05b10b36bcea37564c69c59",
|
||||
"exitCode": 0,
|
||||
"pass": 15
|
||||
},
|
||||
"browserSetupFailure": {
|
||||
"file": "refactor/.cache/site-loading-browser-final.log",
|
||||
"sha256": "32ee26f81405a84496b72482094ff4251cf236cb2cebfefe655c297da50bcbc6",
|
||||
"exitCode": 1,
|
||||
"pass": 20
|
||||
},
|
||||
"browserAccepted": {
|
||||
"file": "refactor/.cache/site-loading-browser-verified.log",
|
||||
"sha256": "10e2712131e6be0dff7a3b2e06641981f11f1e46f0c6b4ec23e52c52d474da8e",
|
||||
"exitCode": 0,
|
||||
"pass": 24
|
||||
},
|
||||
"legacyCommonLintBefore": {
|
||||
"file": "refactor/.cache/site-common-lint-before.log",
|
||||
"sha256": "f1978826cbb118c296332259bdbd3e1caf4a3e6a8aa24b029c21fb13658f034a",
|
||||
"exitCode": 1
|
||||
},
|
||||
"rootLint": {
|
||||
"file": "refactor/.cache/site-loading-root-lint.log",
|
||||
"sha256": "e3083de1ec97688cdfbce56aba620869eea4531afea294e4d5b4840c41e392be",
|
||||
"exitCode": 0
|
||||
},
|
||||
"targetedLint": {
|
||||
"file": "refactor/.cache/site-loading-final-lint.log",
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"exitCode": 0
|
||||
},
|
||||
"browserTypes": {
|
||||
"file": "refactor/.cache/site-loading-types.log",
|
||||
"sha256": "293a5b477d958aca4aaef3ee183447682f361ee94692b533a32c615396ba83b0",
|
||||
"exitCode": 0
|
||||
},
|
||||
"toolTypes": {
|
||||
"file": "refactor/.cache/site-loading-tool-types.log",
|
||||
"sha256": "db6d032c8e69dbfbcdbc37d3dfc6227dce2c7be659fd003c4eca3cc290921771",
|
||||
"exitCode": 0
|
||||
},
|
||||
"toolchain": {
|
||||
"file": "refactor/.cache/site-loading-toolchain.log",
|
||||
"sha256": "4b140bdd27acb3adeb1cf8a328d0024b598c8a4efe1b2c3ae180e9df5a4cb084",
|
||||
"exitCode": 0
|
||||
},
|
||||
"ci": {
|
||||
"file": "refactor/.cache/site-loading-ci-final.log",
|
||||
"sha256": "797184eb9940868fbfc3709895966ecd38c1198fcc56f6ab409d189399d600b2",
|
||||
"exitCode": 0,
|
||||
"pass": 50
|
||||
},
|
||||
"assetsCheck": {
|
||||
"file": "refactor/.cache/site-loading-assets-check.log",
|
||||
"sha256": "80807571a48a18caf82db4ebbe36ec95dcec64372c7d0d1acd4d5984248f1b29",
|
||||
"exitCode": 0
|
||||
}
|
||||
},
|
||||
"browser": {
|
||||
"file": "refactor/.cache/site-loading-browser-accepted/report.json",
|
||||
"sha256": "eeeee6afcf056d50885a8f721f52a4e742d479420930668cc6ebcb9b1b0b0b9e",
|
||||
"stats": {
|
||||
"startTime": "2026-09-13T23:43:41.727Z",
|
||||
"duration": 28844.011,
|
||||
"expected": 24,
|
||||
"skipped": 0,
|
||||
"unexpected": 0,
|
||||
"flaky": 0
|
||||
},
|
||||
"cases": [
|
||||
{
|
||||
"title": "Monaco checks all editor declarations together and runs the Chapter consumer",
|
||||
"project": "chromium",
|
||||
"status": "expected",
|
||||
"attempts": 1,
|
||||
"browser": "153.0.8010.12",
|
||||
"platform": "win32",
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": [
|
||||
{
|
||||
"url": "http://127.0.0.1:8084/test/pattern.mp4",
|
||||
"resourceType": "media",
|
||||
"failure": {
|
||||
"errorText": "net::ERR_ABORTED"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Monaco checks all editor declarations together and runs the Chapter consumer",
|
||||
"project": "firefox",
|
||||
"status": "expected",
|
||||
"attempts": 1,
|
||||
"browser": "155.0",
|
||||
"platform": "win32",
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": []
|
||||
},
|
||||
{
|
||||
"title": "Monaco checks all editor declarations together and runs the Chapter consumer",
|
||||
"project": "webkit",
|
||||
"status": "expected",
|
||||
"attempts": 1,
|
||||
"browser": "26.6",
|
||||
"platform": "win32",
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": []
|
||||
},
|
||||
{
|
||||
"title": "mobile page runs dependencies in order and plays the selected code with no AMD property leak",
|
||||
"project": "chromium",
|
||||
"status": "expected",
|
||||
"attempts": 1,
|
||||
"browser": "153.0.8010.12",
|
||||
"platform": "win32",
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": [
|
||||
{
|
||||
"url": "http://127.0.0.1:8084/test/pattern.mp4",
|
||||
"resourceType": "media",
|
||||
"failure": {
|
||||
"errorText": "net::ERR_ABORTED"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "real script failure is retryable and cancellation restores the original AMD function",
|
||||
"project": "chromium",
|
||||
"status": "expected",
|
||||
"attempts": 1,
|
||||
"browser": "153.0.8010.12",
|
||||
"platform": "win32",
|
||||
"errors": [],
|
||||
"consoleErrors": [
|
||||
"Failed to load resource: the server responded with a status of 404 (Not Found)"
|
||||
],
|
||||
"failedRequests": [
|
||||
{
|
||||
"url": "http://127.0.0.1:8084/site-retry.js",
|
||||
"resourceType": "script",
|
||||
"failure": {
|
||||
"errorText": "net::ERR_ABORTED"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "desktop editor preserves example priority and reuses loaded dependencies when Run is clicked",
|
||||
"project": "chromium",
|
||||
"status": "expected",
|
||||
"attempts": 1,
|
||||
"browser": "153.0.8010.12",
|
||||
"platform": "win32",
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": [
|
||||
{
|
||||
"url": "http://127.0.0.1:8084/test/pattern.mp4",
|
||||
"resourceType": "media",
|
||||
"failure": {
|
||||
"errorText": "net::ERR_ABORTED"
|
||||
}
|
||||
},
|
||||
{
|
||||
"url": "http://127.0.0.1:8084/test/pattern.mp4",
|
||||
"resourceType": "media",
|
||||
"failure": {
|
||||
"errorText": "net::ERR_ABORTED"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Run Code handles nested clicks, encodes dependencies and stays local on 127.0.0.1",
|
||||
"project": "chromium",
|
||||
"status": "expected",
|
||||
"attempts": 1,
|
||||
"browser": "153.0.8010.12",
|
||||
"platform": "win32",
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": []
|
||||
},
|
||||
{
|
||||
"title": "first-language redirect preserves the translated deep link and later explicit Chinese selection",
|
||||
"project": "chromium",
|
||||
"status": "expected",
|
||||
"attempts": 1,
|
||||
"browser": "153.0.8010.12",
|
||||
"platform": "win32",
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": []
|
||||
},
|
||||
{
|
||||
"title": "a late initial example response cannot overwrite a newer editor Run",
|
||||
"project": "chromium",
|
||||
"status": "expected",
|
||||
"attempts": 1,
|
||||
"browser": "153.0.8010.12",
|
||||
"platform": "win32",
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": [
|
||||
{
|
||||
"url": "http://127.0.0.1:8084/test/pattern.mp4",
|
||||
"resourceType": "media",
|
||||
"failure": {
|
||||
"errorText": "net::ERR_ABORTED"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "denied storage keeps the requested documentation page without a redirect loop",
|
||||
"project": "chromium",
|
||||
"status": "expected",
|
||||
"attempts": 1,
|
||||
"browser": "153.0.8010.12",
|
||||
"platform": "win32",
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": []
|
||||
},
|
||||
{
|
||||
"title": "mobile page runs dependencies in order and plays the selected code with no AMD property leak",
|
||||
"project": "firefox",
|
||||
"status": "expected",
|
||||
"attempts": 1,
|
||||
"browser": "155.0",
|
||||
"platform": "win32",
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": []
|
||||
},
|
||||
{
|
||||
"title": "real script failure is retryable and cancellation restores the original AMD function",
|
||||
"project": "firefox",
|
||||
"status": "expected",
|
||||
"attempts": 1,
|
||||
"browser": "155.0",
|
||||
"platform": "win32",
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": []
|
||||
},
|
||||
{
|
||||
"title": "desktop editor preserves example priority and reuses loaded dependencies when Run is clicked",
|
||||
"project": "firefox",
|
||||
"status": "expected",
|
||||
"attempts": 1,
|
||||
"browser": "155.0",
|
||||
"platform": "win32",
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": []
|
||||
},
|
||||
{
|
||||
"title": "Run Code handles nested clicks, encodes dependencies and stays local on 127.0.0.1",
|
||||
"project": "firefox",
|
||||
"status": "expected",
|
||||
"attempts": 1,
|
||||
"browser": "155.0",
|
||||
"platform": "win32",
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": []
|
||||
},
|
||||
{
|
||||
"title": "first-language redirect preserves the translated deep link and later explicit Chinese selection",
|
||||
"project": "firefox",
|
||||
"status": "expected",
|
||||
"attempts": 1,
|
||||
"browser": "155.0",
|
||||
"platform": "win32",
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": []
|
||||
},
|
||||
{
|
||||
"title": "a late initial example response cannot overwrite a newer editor Run",
|
||||
"project": "firefox",
|
||||
"status": "expected",
|
||||
"attempts": 1,
|
||||
"browser": "155.0",
|
||||
"platform": "win32",
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": []
|
||||
},
|
||||
{
|
||||
"title": "denied storage keeps the requested documentation page without a redirect loop",
|
||||
"project": "firefox",
|
||||
"status": "expected",
|
||||
"attempts": 1,
|
||||
"browser": "155.0",
|
||||
"platform": "win32",
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": []
|
||||
},
|
||||
{
|
||||
"title": "mobile page runs dependencies in order and plays the selected code with no AMD property leak",
|
||||
"project": "webkit",
|
||||
"status": "expected",
|
||||
"attempts": 1,
|
||||
"browser": "26.6",
|
||||
"platform": "win32",
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": []
|
||||
},
|
||||
{
|
||||
"title": "real script failure is retryable and cancellation restores the original AMD function",
|
||||
"project": "webkit",
|
||||
"status": "expected",
|
||||
"attempts": 1,
|
||||
"browser": "26.6",
|
||||
"platform": "win32",
|
||||
"errors": [],
|
||||
"consoleErrors": [
|
||||
"Failed to load resource: the server responded with a status of 404 (Not Found)"
|
||||
],
|
||||
"failedRequests": [
|
||||
{
|
||||
"url": "http://127.0.0.1:8084/site-retry.js",
|
||||
"resourceType": "script",
|
||||
"failure": {
|
||||
"errorText": "Load request cancelled"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "desktop editor preserves example priority and reuses loaded dependencies when Run is clicked",
|
||||
"project": "webkit",
|
||||
"status": "expected",
|
||||
"attempts": 1,
|
||||
"browser": "26.6",
|
||||
"platform": "win32",
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": []
|
||||
},
|
||||
{
|
||||
"title": "Run Code handles nested clicks, encodes dependencies and stays local on 127.0.0.1",
|
||||
"project": "webkit",
|
||||
"status": "expected",
|
||||
"attempts": 1,
|
||||
"browser": "26.6",
|
||||
"platform": "win32",
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": []
|
||||
},
|
||||
{
|
||||
"title": "first-language redirect preserves the translated deep link and later explicit Chinese selection",
|
||||
"project": "webkit",
|
||||
"status": "expected",
|
||||
"attempts": 1,
|
||||
"browser": "26.6",
|
||||
"platform": "win32",
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": []
|
||||
},
|
||||
{
|
||||
"title": "a late initial example response cannot overwrite a newer editor Run",
|
||||
"project": "webkit",
|
||||
"status": "expected",
|
||||
"attempts": 1,
|
||||
"browser": "26.6",
|
||||
"platform": "win32",
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": []
|
||||
},
|
||||
{
|
||||
"title": "denied storage keeps the requested documentation page without a redirect loop",
|
||||
"project": "webkit",
|
||||
"status": "expected",
|
||||
"attempts": 1,
|
||||
"browser": "26.6",
|
||||
"platform": "win32",
|
||||
"errors": [],
|
||||
"consoleErrors": [],
|
||||
"failedRequests": []
|
||||
}
|
||||
],
|
||||
"initialArchive": "refactor/.cache/site-loading-browser-initial",
|
||||
"setupFailureArchive": "refactor/.cache/site-loading-browser-setup-failure",
|
||||
"failure": {
|
||||
"project": "firefox",
|
||||
"phase": "page fixture setup before test body",
|
||||
"message": "Test timeout of 20000ms exceeded while setting up page",
|
||||
"simultaneousBaseline": true,
|
||||
"causalAttribution": "Not proven; subsequent run was serialized after the baseline. No timeout, retry or skip change."
|
||||
}
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"file": "docs/index.html",
|
||||
"sha256Lf": "f654fa04c8aea08bc718e96ce2a438f04cc91060b2bb9b2b86f4e1467258fd97"
|
||||
},
|
||||
{
|
||||
"file": "docs/assets/js/common.js",
|
||||
"sha256Lf": "c18f72dd5489bdee5b7bd0030c716eab08ac7ea7bd253353eaf406fb8ae2bc14"
|
||||
},
|
||||
{
|
||||
"file": "package.json",
|
||||
"sha256Lf": "5d07a142cd37b886cbad801a2dd48ec6b6a55a40aebebacf206b1370688d9214"
|
||||
},
|
||||
{
|
||||
"file": "scripts/build-site-assets.mjs",
|
||||
"sha256Lf": "58c163e16efa901d227045b468f324ac79ad4ab2f8ab8716f10a8ede62091666"
|
||||
},
|
||||
{
|
||||
"file": "scripts/tsconfig.docs.json",
|
||||
"sha256Lf": "47895ead75f89bed4ec707b77b6b9daa3705b31f3d241c23e83d22b61d565359"
|
||||
},
|
||||
{
|
||||
"file": "test/site-loading.test.js",
|
||||
"sha256Lf": "cd0e513fcc5a6c69458ee6dd4ac12a270a2d28c15e13c46c0bab1ef1299b8667"
|
||||
},
|
||||
{
|
||||
"file": "test/browser/site-loading.spec.js",
|
||||
"sha256Lf": "3038e871b44d928fe537f15d20c88f541a7f73650b7fad46348739cfad535a57"
|
||||
},
|
||||
{
|
||||
"file": "test/browser/editor-declarations.spec.js",
|
||||
"sha256Lf": "3e80ea68e869e29e1a0230759bb1dc02d503f393de41b03ded5296b607e44684"
|
||||
},
|
||||
{
|
||||
"file": "packages/artplayer-vitepress/browser/editor-loader.ts",
|
||||
"sha256Lf": "7cfdc82726d164d450b21680d950ec3f508482f1ab743febee473855248568e1"
|
||||
},
|
||||
{
|
||||
"file": "packages/artplayer-vitepress/browser/loader.ts",
|
||||
"sha256Lf": "d5d5c079854f40025be6af457baba863ab8cd37af59c120e811b7f59e5c160b6"
|
||||
},
|
||||
{
|
||||
"file": "packages/artplayer-vitepress/browser/mobile.ts",
|
||||
"sha256Lf": "beba095b7fda9fb9aa2c5cd50437ea9e186fa76e700e6e79ee8e9eb67fd86d7d"
|
||||
},
|
||||
{
|
||||
"file": "packages/artplayer-vitepress/browser/navigation.ts",
|
||||
"sha256Lf": "1ad87a6bee717f9e62d28c5e6dc382c5c1b43c09c9f10c64e3ef0824f2fbdedc"
|
||||
},
|
||||
{
|
||||
"file": "packages/artplayer-vitepress/browser/README.md",
|
||||
"sha256Lf": "93c4b79c70e8a43ac4999642171366d272a0868907cf7e909ec9be9ed9f6562d"
|
||||
},
|
||||
{
|
||||
"file": "packages/artplayer-vitepress/browser/tsconfig.json",
|
||||
"sha256Lf": "9a1331ca4b311d66df7aca6e5ff8a8e3647e338931e013ffc7df58aaee8f6a0c"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"file": "docs/assets/js/loader.js",
|
||||
"sha256Lf": "f917507fc921f96159402b7f744dc15b8341a89cf0e089abbfe459879169d1a5"
|
||||
},
|
||||
{
|
||||
"file": "docs/assets/js/mobile.js",
|
||||
"sha256Lf": "53a03d1c6725942ee306d2d38fb7ee31d756f16d68f7e0e38f0f01d6073dbf47"
|
||||
},
|
||||
{
|
||||
"file": "packages/artplayer-vitepress/docs/public/main.js",
|
||||
"sha256Lf": "b8c9b9334abc800e12fd2b8154d89f04c580adf0a155718f2ed44e85f7fd44d7"
|
||||
}
|
||||
],
|
||||
"limitations": [
|
||||
"Baseline 522 passed before final common.js const/layout cleanup; affected editor and site consumers then revalidated.",
|
||||
"Common desktop UI remains JavaScript and is explicitly assigned to SITE-03.",
|
||||
"No advertising-service, real device/BFCache, full VitePress/docs/document build or remote CI acceptance.",
|
||||
"Cancellation test controls append; removing a classic script does not guarantee cancellation of already-started evaluation.",
|
||||
"No manual Chrome/iab session, push, translation request or publication."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
# SITE-LOAD-01 文档示例加载与导航
|
||||
|
||||
起点 `76499e7a3e033c8350ff495ba8bfff4ca5c08d25`。从 SITE-03 分出浏览器加载
|
||||
与导航,保留父任务的 i18n/文档/LLM/翻译流程,并明确桌面剩余 UI 的 TS 迁移。
|
||||
没有修改核心/插件运行代码、公开类型、样式钩子、包入口、版本或锁文件。
|
||||
|
||||
## 实现与兼容边界
|
||||
|
||||
- `packages/artplayer-vitepress/browser/` 新增 TS loader、桌面桥接、移动入口和
|
||||
导航模块。README 解释生命周期、生成规则、已验证边界与剩余任务。原 mobile.js
|
||||
和 public/main.js 变为生成资产;新增桌面 loader.js 在 index.html 原引导链前
|
||||
加载。移动端内联同一模块,保持原页面和查询 URL。
|
||||
- 固定旧提交复现:移动 loadScript 失败不恢复 define;两次同时加载各自保存
|
||||
临时 define 后,Promise.all 成功也可能留下 undefined。新 loader 按输入顺序
|
||||
串行所有批次,成功绝对 URL 缓存、失败移除并可重试;CSS 保持顺序但不改 AMD。
|
||||
在成功、失败、append 抛错和取消时恢复原 define 描述符或原本不存在的状态。
|
||||
不兼容的不可变属性在插入前失败,不静默绕过。
|
||||
- dispose 拒绝等待项与未来调用并拆监听器/待加载元素。已加载元素仍归页面。
|
||||
移除 script 不保证浏览器取消已经开始的脚本求值;第三方不可逆全局副作用
|
||||
不承诺回滚。非 BFCache 的 pagehide 清理,persisted 页保留可恢复 loader。
|
||||
真实设备和 BFCache 行为没有计为通过。
|
||||
- 桌面保留 Monaco、Run、导入、存储和现有 cleanup/destroy 顺序,替换重复 loader。
|
||||
初始化代次阻止晚到的旧示例覆盖新 Run。example 仍优先于 code;移动/桌面默认
|
||||
示例仍是 mobile/index;HTTP 错误不作为代码执行。查询保留原始 `+`、最后重复
|
||||
参数和一次显式解码。移动示例在普通非严格函数作用域运行,不变成 ES module。
|
||||
- Run Code 编码 libs/code,支持子元素点击并处理缺失代码节点;localhost、
|
||||
127.0.0.1、IPv6 loopback 均用自身 host 的 HTTP 8082,非本地仍用旧官方编辑器。
|
||||
首次非中文访问保留现存英文对应页与 query/hash,缺对应页回英文首页;英文页
|
||||
不再自跳,lang-init 原 key/值保留,显式后续中文选择不被覆盖。存储不可用时
|
||||
保留请求页,防止循环。英文路径从实际 Markdown 清单生成,未虚构翻译页面。
|
||||
- common.js 原有 32 项目标 lint 错误已处理:const/布局、声明实际外部 globals,
|
||||
eval 仅针对编辑器原 Run 作用域保留定向说明。该旧 UI 文件纳入根 lint;未把
|
||||
它计为完整 TS 迁移。libUris 改 const 后,其实际加载列表测试同时支持 let/const。
|
||||
|
||||
## 工程与验证
|
||||
|
||||
无新依赖,复用固定 esbuild 生成 ES2020 经典脚本。build:site-assets/check:site-assets
|
||||
分别生成/只读核对三个资产,后者进入 CI;typecheck:site-assets 严格检查浏览器
|
||||
模块,生成 CLI 在 docs-tools 下 checkJs。root build:docs 先生成资产,直接 workspace
|
||||
VitePress 命令的编排仍属父任务;没有在本任务暗中请求翻译服务。
|
||||
|
||||
- 5 项新增 Node 回归验证旧问题、串行/去重/重试/取消、描述符边界、URL 与语言规则。
|
||||
最终连同编辑器、文档退出码、清单/示例回归共 13 项通过。完整 baseline 522
|
||||
通过(在最终 common.js 的 const/布局清理前;相关最终消费者已独立回归)。
|
||||
- 首轮三浏览器 15 项通过。扩展运行 20 通过/1 失败,唯一失败是 Firefox 在
|
||||
page fixture 创建时超时 20000ms,尚未进入测试代码;当时完整基线并行运行。
|
||||
不能据此断言唯一根因是 CPU。保留 trace/report,等基线完成后串行运行,不
|
||||
改 timeout/retry/skip;最终 24 通过(21 新加载/导航 + 3 Monaco 回归),0 跳过。
|
||||
- 浏览器实际访问 root/mobile 页面与本地 Monaco;移动播放推进、暂停、销毁;
|
||||
桌面依赖复用、example 优先、重复 Run 清理、旧响应隔离;原生脚本 404/重试
|
||||
和 AMD 恢复。取消测试控制 append 阶段,只证明回收/Promise,不证明取消已
|
||||
请求脚本的执行。Run Code 使用真实点击,语言重定向用受控 HTTPS 页面路由。
|
||||
- 根 lint 0 error/1 既有生成声明 warning;目标 lint、两套工具/浏览器严格类型、
|
||||
资产只读一致性、严格 Node24.21.0/Yarn1.22.22 工具链、CI 50 项通过。
|
||||
具体日志/源与产物/浏览器版本指纹见验证 JSON。
|
||||
|
||||
外部分析/广告脚本在自动化路线中为空响应,没有修改生产广告配置。未验证
|
||||
广告服务、所有 233 示例、真实设备、完整 VitePress 构建/生成 docs/document、
|
||||
遠端 CI/Pages/npm;没有手动 Chrome/iab 会话、推送或发布。此次测试不是这些
|
||||
范围的替代证据。SITE-03/04/05、EX-03 和包验收仍开放。
|
||||
|
||||
本任务单独提交并审计。回退提交可恢复原页面/脚本和根命令;会带回旧 define
|
||||
失败与乱序问题,应优先针对回归修复。修改 TS 后重新 build:site-assets,不手改
|
||||
生成资产;维护实际模块 README、任务、当前清单和独立变更记录。
|
||||
@@ -12,6 +12,8 @@
|
||||
| `yarn typecheck:react` / `yarn typecheck:vue` | 原 React TSX / Vue SFC 示例严格检查,ci:check 同时执行对应 lint |
|
||||
| `yarn typecheck:docs-tools` / `yarn check:docs-smoke` | 严格检查 TS 示例/声明生成器及 JS/MJS 门面;只读核对确定性生成的 readiness smoke,ci:check 执行 |
|
||||
| `yarn check:editor-types` | 只读核对全部编辑器声明、SDK notices 和实际 libUris;主 TS 5.9.3/历史 4.3.5 整组语义检查,ci:check 执行 |
|
||||
| `yarn build:site-assets` / `yarn check:site-assets` | 从站点 browser/ TS 生成三个经典脚本;check 只读,root build:docs 先生成;完整 VitePress 构建另行验收 |
|
||||
| `yarn typecheck:site-assets` | 严格检查站点 browser/ 模块;生成脚本由 typecheck:docs-tools 的 checkJs 覆盖;均进入 ci:check |
|
||||
| `yarn test:react-consumer` / `yarn test:vue-consumer` | 仓库外 tarball 安装、原框架示例、开发/生产三引擎;browser-smoke 执行并上传独立目录,本地证据不代替远端矩阵 |
|
||||
| `yarn test:unit` | 原播放/DASH 回归、同夹具的新旧公共契约与 JS/TS loader 验证 |
|
||||
| `yarn test:node` | test:unit 加工具链/文档构建回归,保留原 test:playback/test:dash-control 入口 |
|
||||
|
||||
+6
-4
@@ -2,9 +2,9 @@
|
||||
|
||||
> 由 tasks.json 生成。请修改数据后运行 `node refactor/scripts/plan.mjs --write`,不要手改本表。
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 240 项,范围 22 个包及工作区/示例。
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 241 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 59 / doing 15 / blocked 0 / done 166 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 59 / doing 15 / blocked 0 / done 167 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
| artplayer-proxy-mediabunny | 1.2.0 | PKG-MB-01, PKG-MB-02, PKG-MB-03, PKG-MB-04, PKG-MB-05, PKG-MB-06, PKG-MB-07, PKG-MB-08, PKG-MB-09, PKG-MB-10 |
|
||||
| artplayer-tool-iframe | 1.1.0 | PKG-IFRAME-01, PKG-IFRAME-02, PKG-IFRAME-03, PKG-IFRAME-04, PKG-IFRAME-05, PKG-IFRAME-06 |
|
||||
| artplayer-tool-thumbnail | 4.4.0 | PKG-TOOL-THUMB-01, PKG-TOOL-THUMB-02, PKG-TOOL-THUMB-03, PKG-TOOL-THUMB-04, PKG-TOOL-THUMB-05, PKG-TOOL-THUMB-06 |
|
||||
| artplayer-vitepress | 1.1.0 | SITE-01, SITE-SMOKE-01, SITE-02, SITE-03, SITE-04, SITE-05, SITE-06, SITE-07 |
|
||||
| artplayer-vitepress | 1.1.0 | SITE-01, SITE-SMOKE-01, SITE-02, SITE-LOAD-01, SITE-03, SITE-04, SITE-05, SITE-06, SITE-07 |
|
||||
|
||||
## 0 规划
|
||||
|
||||
@@ -383,7 +383,8 @@
|
||||
| SITE-01 | artplayer-vitepress, workspace<br>清点文档/示例/生成链 | BASE-04, BASE-05 | 中英文文档、插件页面、demo URL、编辑器声明与生成目录清单;六类 HTML 入口、prod/libs/code/example 加载行为及移动重定向 | 所有公开 API/插件有对应页面或明确补充任务;核实 BASE-DEMO-01 的历史 thumbnail 插件来源及 29 示例/36 HTML 路径;明确 BASE-05 所记录站点实际分发与未设 private 的 manifest 意图;接续 VENDOR-05/06/07/08 和 BASE-SITE-01/BASE-MEDIA-01,核对字体、Monaco、vConsole、console bundle 与样本来源及分发范围 | M | done |
|
||||
| SITE-SMOKE-01 | artplayer-vitepress, workspace<br>迁移示例生成器并验证真实就绪与清理 | SITE-01 | TS Markdown 解析/生成与浏览器运行模块;旧命令和 URL、稳定实例就绪 smoke、错误与资源清理 | 复现旧 malformed 死循环并保证有限失败;保留全部既有示例内容、生成确定且 check 只读;不再以 100ms 作为成功,验证原生就绪、异常、取消与清理;三浏览器检查,明确延迟交互与全部 233 示例仍需 EX-03 | M | done |
|
||||
| SITE-02 | artplayer-vitepress<br>整理声明与示例生成器 | SITE-01, ENG-04, ENG-06, SITE-SMOKE-01 | build-ts/build-test 生成链的可验证 TS 脚本 | 不靠字符串拼接掩盖声明错误,生成示例有真实断言或仅标 smoke;替换固定 100ms 成功判定,明确异步错误、清理和生成覆盖限制;坏代码块解析必须终止,覆盖 malformed 分支不推进 regexp 的回归 | M | done |
|
||||
| SITE-03 | artplayer-vitepress<br>整理 i18n/文档/LLM 生成流程 | SITE-02 | build-i18n/build-docs/build-llm/trans-docs 的任务边界和错误处理 | 原命令兼容、生成可复现,翻译步骤不隐式运行远程服务;覆盖移动 loader 失败恢复 define、脚本依赖顺序、localhost/127.0.0.1 Run Code 目标和语言重定向 | M | todo |
|
||||
| SITE-LOAD-01 | artplayer-vitepress, workspace<br>修复文档示例加载顺序与导航边界 | SITE-02 | 共享 TS loader、移动入口与 Run Code/语言路由,保留 URL 和使用方式 | 旧版复现 define 丢失和依赖乱序;真实浏览器验证成功/失败恢复、顺序、去重、编码/优先级及 localhost/127 路由和语言跳转;不以此替代 SITE-03 构建/翻译流程 | M | done |
|
||||
| SITE-03 | artplayer-vitepress<br>整理 i18n/文档/LLM 生成流程 | SITE-02, SITE-LOAD-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-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 |
|
||||
@@ -609,6 +610,7 @@
|
||||
- SITE-01: [记录](site-inventory.md) [记录](baselines/site-inventory.json) [记录](baselines/site-provenance.json) [记录](baselines/demo-additions.json) [记录](changes/2026-09-14-SITE-01-site-inventory.md) [记录](baselines/site-inventory-validation.json)
|
||||
- SITE-SMOKE-01: [记录](changes/2026-09-14-SITE-SMOKE-01-documentation-smoke.md) [记录](baselines/docs-smoke-validation.json) [记录](scripts/docs-smoke.test.mjs)
|
||||
- SITE-02: [记录](changes/2026-09-14-SITE-02-editor-declarations.md) [记录](baselines/editor-declarations-validation.json) [记录](changes/2026-09-14-SITE-SMOKE-01-documentation-smoke.md)
|
||||
- SITE-LOAD-01: [记录](changes/2026-09-14-SITE-LOAD-01-site-loading.md) [记录](baselines/site-loading-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-LOAD-01 示例加载与导航完成
|
||||
|
||||
从 SITE-03 拆出共享 TS loader、移动入口与 Run Code/语言导航;恢复 define 原
|
||||
描述符,串行依赖/批次并缓存成功项、重试失败项,阻止旧示例晚到覆盖新 Run。
|
||||
保留原 URL、example 优先、Run cleanup 和页面使用方式。生成三个 ES2020
|
||||
资产,root build:docs 先生成;浏览器类型、生成一致性与 common.js lint 已纳入
|
||||
CI。13 项最终目标测试、522 项 baseline、50 项 CI 回归通过。三浏览器最终
|
||||
24 项通过(21 新项 + 3 Monaco);一次 Firefox page fixture 创建超时保留报告,
|
||||
其后等基线结束串行复验,没有扩大超时/重试取绿。类型、只读资产、工具链、
|
||||
lint 均通过(根 lint 1 条既有 warning)。见
|
||||
[变更](changes/2026-09-14-SITE-LOAD-01-site-loading.md)和
|
||||
[证据](baselines/site-loading-validation.json)。当前 241 项:167 done、15 doing、
|
||||
59 todo。SITE-03 保留生成/翻译流程与桌面剩余 UI 的 TS 迁移;未验证完整
|
||||
VitePress 构建/设备/广告服务/远端 CI。下一步接续 SITE-03,不改变 VAST 默认
|
||||
行为和 Auto Thumbnail 首帧的开放状态。仅本地独立提交,无推送或发布。
|
||||
|
||||
## SITE-02 编辑器声明生成链完成
|
||||
|
||||
build:ts 与 core/plugin 转换器完成 TS 模块拆分,原 JS/MJS 命令/导入路径保留。
|
||||
|
||||
@@ -78,7 +78,7 @@ export function captureSiteInventory() {
|
||||
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' })),
|
||||
scripts: [...scriptFiles, ...files('scripts/docs-smoke').filter(file => file.endsWith('.ts')), ...files('scripts/editor-declarations').filter(file => file.endsWith('.ts'))].map(file => ({ file, sha256Lf: hash(read(file)) })),
|
||||
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('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'),
|
||||
assets,
|
||||
|
||||
@@ -8,6 +8,10 @@ SITE-SMOKE-01 后续已将 build:test 拆为 TS 解析、生成和浏览器运
|
||||
不再走删除 import 的文本回退。见[模块说明](../scripts/editor-declarations/README.md)。
|
||||
以下 SITE-01 对这些旧生成问题的描述是历史发现,站点/编辑器交互仍需后续验收。
|
||||
|
||||
SITE-LOAD-01 后续修复并抽取共享 loader、移动入口和 Run Code/语言导航,TS
|
||||
来源及生成规则见[浏览器模块](../packages/artplayer-vitepress/browser/README.md)。
|
||||
保留本站点生成/翻译流程和桌面剩余 UI 给 SITE-03,完整内容/页面验收给 SITE-04/05。
|
||||
|
||||
SITE-01 在 `d62ab13a35c756ea567c426d4fbe00d893dd7e2b` 后核对当前源码。
|
||||
可重跑清单见 [site-inventory.json](baselines/site-inventory.json);它登记
|
||||
22 包、27 Markdown、30 示例、36 HTML、963 条声明成员和 204 个资产指纹。
|
||||
|
||||
+24
-3
@@ -4148,6 +4148,26 @@
|
||||
"changes/2026-09-14-SITE-SMOKE-01-documentation-smoke.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "SITE-LOAD-01",
|
||||
"phase": "6 文档与消费者",
|
||||
"title": "修复文档示例加载顺序与导航边界",
|
||||
"scope": [
|
||||
"artplayer-vitepress",
|
||||
"workspace"
|
||||
],
|
||||
"dependsOn": [
|
||||
"SITE-02"
|
||||
],
|
||||
"status": "done",
|
||||
"risk": "M",
|
||||
"deliverable": "共享 TS loader、移动入口与 Run Code/语言路由,保留 URL 和使用方式",
|
||||
"acceptance": "旧版复现 define 丢失和依赖乱序;真实浏览器验证成功/失败恢复、顺序、去重、编码/优先级及 localhost/127 路由和语言跳转;不以此替代 SITE-03 构建/翻译流程",
|
||||
"evidence": [
|
||||
"changes/2026-09-14-SITE-LOAD-01-site-loading.md",
|
||||
"baselines/site-loading-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "SITE-03",
|
||||
"phase": "6 文档与消费者",
|
||||
@@ -4156,12 +4176,13 @@
|
||||
"artplayer-vitepress"
|
||||
],
|
||||
"dependsOn": [
|
||||
"SITE-02"
|
||||
"SITE-02",
|
||||
"SITE-LOAD-01"
|
||||
],
|
||||
"status": "todo",
|
||||
"risk": "M",
|
||||
"deliverable": "build-i18n/build-docs/build-llm/trans-docs 的任务边界和错误处理",
|
||||
"acceptance": "原命令兼容、生成可复现,翻译步骤不隐式运行远程服务;覆盖移动 loader 失败恢复 define、脚本依赖顺序、localhost/127.0.0.1 Run Code 目标和语言重定向",
|
||||
"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": []
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { build } from 'esbuild'
|
||||
import { globSync } from 'glob'
|
||||
|
||||
assert(process.argv.slice(2).every(arg => arg === '--check'), 'Use yarn build:site-assets [--check]')
|
||||
const root = 'packages/artplayer-vitepress'
|
||||
const englishPaths = globSync('**/*.md', { cwd: `${root}/docs/en`, posix: true }).sort().flatMap(file => file === 'index.md' ? ['', 'index.html'] : [file.replace(/\.md$/, '.html'), file.replace(/\.md$/, '')])
|
||||
/** @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`]]
|
||||
for (const [entry, output] of entries) {
|
||||
const result = await build({
|
||||
...(entry === 'navigation.ts'
|
||||
? { stdin: { contents: `import { installNavigation } from './navigation.ts'; installNavigation(window, ${JSON.stringify(englishPaths)});`, resolveDir: path.resolve(root, 'browser'), loader: 'ts' } }
|
||||
: { entryPoints: [path.join(root, 'browser', entry)] }),
|
||||
bundle: true,
|
||||
write: false,
|
||||
format: 'iife',
|
||||
target: 'es2020',
|
||||
legalComments: 'none',
|
||||
logLevel: 'silent',
|
||||
})
|
||||
assert(result.outputFiles[0], 'Missing site bundle')
|
||||
outputs.set(output, `// Generated by yarn build:site-assets. Do not edit.\n${result.outputFiles[0].text}`)
|
||||
}
|
||||
for (const [file, content] of outputs) {
|
||||
if (process.argv.includes('--check'))
|
||||
assert.equal(fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n'), content, `Site asset drift: ${file}`)
|
||||
else fs.writeFileSync(file, content)
|
||||
}
|
||||
console.log(`Site browser assets ${process.argv.includes('--check') ? 'checked' : 'generated'}: ${outputs.size}; English routes: ${englishPaths.length}`)
|
||||
@@ -16,6 +16,7 @@
|
||||
"docs-smoke/**/*.ts",
|
||||
"build-test.js",
|
||||
"build-ts.js",
|
||||
"build-site-assets.mjs",
|
||||
"editor-declarations/**/*.ts",
|
||||
"editor-types.mjs",
|
||||
"plugin-editor-types.mjs"
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Tests and fixture ownership
|
||||
|
||||
`site-loading.test.js` freezes the old mobile loader's failure/race reproduction
|
||||
and checks shared loader ownership, retries/cancellation, query encoding and
|
||||
language rules. Actual local pages and Monaco are covered by site-loading.spec.js.
|
||||
|
||||
Editor tooling lives in `scripts/editor-declarations/`; see its README.
|
||||
`test/editor-types.test.js` checks standalone globals and module consumers with
|
||||
current/old compilers, Chapter/VAST regressions, generated files and library-list
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# Browser regression entry
|
||||
|
||||
`site-loading.spec.js` covers actual desktop/mobile loading, dependency order,
|
||||
failure/retry/AMD restoration, example priority, repeated Run and stale responses,
|
||||
plus Run Code and language routing. External analytics/ads are inert test routes;
|
||||
no advertising service or native device acceptance is implied. Keep this browser
|
||||
run serial with heavy compiler baselines after observing local setup contention.
|
||||
|
||||
`editor-declarations.spec.js` loads the repository Monaco with the 22 declarations
|
||||
actually listed in `common.js`, checks positive/negative consumers and runs emitted
|
||||
Chapter code through ready/destroy with controlled media. VAST coverage is types
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 list = common.match(/let libUris = \[([\s\S]*?)\]/)?.[1] || ''
|
||||
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)
|
||||
await testInfo.attach('editor-declaration-inputs', { contentType: 'application/json', body: JSON.stringify(Object.fromEntries(names.map(name => [name, hash(fs.readFileSync(`docs/assets/ts/${name}`))]))) })
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
import fs from 'node:fs'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
test('mobile page runs dependencies in order and plays the selected code with no AMD property leak', async ({
|
||||
page,
|
||||
}) => {
|
||||
let release
|
||||
let started
|
||||
const gate = new Promise((resolve) => {
|
||||
release = resolve
|
||||
})
|
||||
const firstRequested = new Promise((resolve) => {
|
||||
started = resolve
|
||||
})
|
||||
let secondRequested = false
|
||||
await page.route('https://**/*', route =>
|
||||
route.fulfill({ status: 200, body: '' }))
|
||||
await page.route('**/site-first.js', async (route) => {
|
||||
started()
|
||||
await gate
|
||||
await route.fulfill({
|
||||
contentType: 'text/javascript',
|
||||
body: 'window.order = ["first"]; window.firstDependency = true;',
|
||||
})
|
||||
})
|
||||
await page.route('**/site-second.js', (route) => {
|
||||
secondRequested = true
|
||||
return route.fulfill({
|
||||
contentType: 'text/javascript',
|
||||
body: 'if (!window.firstDependency) throw new Error("Dependency order"); window.order.push("second");',
|
||||
})
|
||||
})
|
||||
const code
|
||||
= 'window.order.push("code"); window.art = new Artplayer({ container: ".artplayer-app", url: "/test/pattern.mp4", muted: true });'
|
||||
await page.goto(
|
||||
`/mobile.html?libs=${encodeURIComponent('./site-first.js\n./site-second.js')}&code=${encodeURIComponent(code)}`,
|
||||
{ waitUntil: 'domcontentloaded' },
|
||||
)
|
||||
await firstRequested
|
||||
expect(secondRequested).toBe(false)
|
||||
release()
|
||||
await page.waitForFunction(() => window.art?.isReady)
|
||||
expect(await page.evaluate(() => window.order)).toEqual([
|
||||
'first',
|
||||
'second',
|
||||
'code',
|
||||
])
|
||||
expect(await page.evaluate(() => Object.hasOwn(window, 'define'))).toBe(
|
||||
false,
|
||||
)
|
||||
await page.evaluate(() => window.art.play())
|
||||
await expect
|
||||
.poll(() => page.evaluate(() => window.art.currentTime))
|
||||
.toBeGreaterThan(0)
|
||||
await page.evaluate(() => window.art.pause())
|
||||
expect(await page.evaluate(() => window.art.playing)).toBe(false)
|
||||
await page.evaluate(() => window.art.destroy(true))
|
||||
expect(await page.evaluate(() => window.Artplayer.instances.length)).toBe(0)
|
||||
})
|
||||
|
||||
test('real script failure is retryable and cancellation restores the original AMD function', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('/test/player.html')
|
||||
await page.addScriptTag({ url: '/assets/js/loader.js' })
|
||||
let attempts = 0
|
||||
await page.route('**/site-retry.js', route =>
|
||||
route.fulfill(
|
||||
++attempts === 1
|
||||
? { status: 404, body: 'missing' }
|
||||
: {
|
||||
contentType: 'text/javascript',
|
||||
body: 'window.retryLoaded = true;',
|
||||
},
|
||||
))
|
||||
const result = await page.evaluate(async () => {
|
||||
const original = Object.assign(() => {}, { amd: {} })
|
||||
Object.defineProperty(window, 'define', {
|
||||
value: original,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
})
|
||||
let error = ''
|
||||
try {
|
||||
await window.ArtplayerDocsLoader.loadLibraries('/site-retry.js')
|
||||
}
|
||||
catch (cause) {
|
||||
error = cause.message
|
||||
}
|
||||
const restored
|
||||
= window.define === original
|
||||
&& !Object.getOwnPropertyDescriptor(window, 'define').enumerable
|
||||
await window.ArtplayerDocsLoader.loadLibraries('/site-retry.js')
|
||||
await window.ArtplayerDocsLoader.loadLibraries('/site-retry.js')
|
||||
return {
|
||||
error,
|
||||
restored,
|
||||
retryLoaded: window.retryLoaded,
|
||||
nodes: document.querySelectorAll('script[src$="site-retry.js"]').length,
|
||||
after: window.define === original,
|
||||
}
|
||||
})
|
||||
expect(result.error).toContain('Loading script failed')
|
||||
expect(result).toMatchObject({
|
||||
restored: true,
|
||||
retryLoaded: true,
|
||||
nodes: 1,
|
||||
after: true,
|
||||
})
|
||||
expect(attempts).toBe(2)
|
||||
const cancellation = await page.evaluate(async () => {
|
||||
const original = window.define
|
||||
const append = document.head.appendChild
|
||||
document.head.appendChild = function (node) {
|
||||
if (node.src?.endsWith('/site-pending.js')) {
|
||||
queueMicrotask(() => window.ArtplayerDocsLoader.dispose())
|
||||
return node
|
||||
}
|
||||
return append.call(this, node)
|
||||
}
|
||||
try {
|
||||
let error = ''
|
||||
try {
|
||||
await window.ArtplayerDocsLoader.loadLibraries('/site-pending.js')
|
||||
}
|
||||
catch (cause) {
|
||||
error = cause.message
|
||||
}
|
||||
return { error, restored: window.define === original }
|
||||
}
|
||||
finally {
|
||||
document.head.appendChild = append
|
||||
}
|
||||
})
|
||||
expect(cancellation.error).toContain('Cancelled')
|
||||
expect(cancellation.restored).toBe(true)
|
||||
})
|
||||
|
||||
test('desktop editor preserves example priority and reuses loaded dependencies when Run is clicked', async ({
|
||||
page,
|
||||
}) => {
|
||||
let loads = 0
|
||||
await page.route('https://**/*', route =>
|
||||
route.fulfill({ status: 200, body: '' }))
|
||||
await page.route('**/site-lib.js', (route) => {
|
||||
loads++
|
||||
return route.fulfill({
|
||||
contentType: 'text/javascript',
|
||||
body: 'window.siteLibraryLoaded = true;',
|
||||
})
|
||||
})
|
||||
await page.route('**/assets/example/site.fixture.js', route =>
|
||||
route.fulfill({
|
||||
contentType: 'text/javascript',
|
||||
body: 'if (!window.siteLibraryLoaded) throw new Error("Missing dependency"); window.siteRuns = (window.siteRuns || 0) + 1; var art = new Artplayer({ container: ".artplayer-app", url: "/test/pattern.mp4", muted: true });',
|
||||
}))
|
||||
await page.goto(
|
||||
`/?libs=${encodeURIComponent('./site-lib.js')}&example=site.fixture&code=${encodeURIComponent('throw new Error("wrong priority")')}`,
|
||||
{ waitUntil: 'domcontentloaded' },
|
||||
)
|
||||
await page.waitForFunction(() => window.art?.isReady)
|
||||
const before = await page.evaluate(() => {
|
||||
window.previousArt = window.art
|
||||
return window.art.id
|
||||
})
|
||||
await page.locator('.run').click()
|
||||
await page.waitForFunction(
|
||||
id => window.art?.isReady && window.art.id !== id,
|
||||
before,
|
||||
)
|
||||
expect(
|
||||
await page.evaluate(() => ({
|
||||
runs: window.siteRuns,
|
||||
destroyed: window.previousArt.isDestroy,
|
||||
instances: window.Artplayer.instances.length,
|
||||
amd: typeof window.define,
|
||||
})),
|
||||
).toEqual({ runs: 2, destroyed: true, instances: 1, amd: 'function' })
|
||||
expect(loads).toBe(1)
|
||||
await page.evaluate(() => window.art.destroy(true))
|
||||
})
|
||||
|
||||
test('Run Code handles nested clicks, encodes dependencies and stays local on 127.0.0.1', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('/test/player.html')
|
||||
await page.evaluate(() => {
|
||||
window.opened = []
|
||||
window.open = (url) => {
|
||||
window.opened.push(url)
|
||||
return null
|
||||
}
|
||||
document.body.innerHTML
|
||||
= '<div className="run-code" data-libs="https://cdn.example/lib.js?a=1&b=2"><span id="run-inner">Run</span></div><div><pre><code>var x = "你好+a&b";</code></pre></div><div class="run-code" id="missing">Missing code</div>'
|
||||
})
|
||||
const code = fs.readFileSync(
|
||||
'packages/artplayer-vitepress/docs/public/main.js',
|
||||
'utf8',
|
||||
)
|
||||
await page.addScriptTag({ content: code })
|
||||
await page.addScriptTag({ content: code })
|
||||
await page.locator('#run-inner').click()
|
||||
await page.locator('#missing').click()
|
||||
const opened = await page.evaluate(() => window.opened)
|
||||
expect(opened).toHaveLength(1)
|
||||
const url = new URL(opened[0])
|
||||
expect(url.origin).toBe('http://127.0.0.1:8082')
|
||||
expect(url.searchParams.get('libs')).toBe(
|
||||
'https://cdn.example/lib.js?a=1&b=2',
|
||||
)
|
||||
expect(url.searchParams.get('code')).toBe('var x = "你好+a&b";')
|
||||
expect(page.url()).toContain('/test/player.html')
|
||||
})
|
||||
|
||||
test('first-language redirect preserves the translated deep link and later explicit Chinese selection', async ({
|
||||
page,
|
||||
}) => {
|
||||
const code = fs.readFileSync(
|
||||
'packages/artplayer-vitepress/docs/public/main.js',
|
||||
'utf8',
|
||||
)
|
||||
await page.addInitScript(() =>
|
||||
Object.defineProperty(navigator, 'language', { get: () => 'en-US' }),
|
||||
)
|
||||
await page.route('https://docs.example/**', route =>
|
||||
route.fulfill({
|
||||
contentType: 'text/html',
|
||||
body: `<html><body>Documentation<script>${code}</script></body></html>`,
|
||||
}))
|
||||
await page.goto('https://docs.example/document/start/option.html?x=1#url')
|
||||
await page.waitForURL(
|
||||
'https://docs.example/document/en/start/option.html?x=1#url',
|
||||
)
|
||||
expect(await page.evaluate(() => localStorage.getItem('lang-init'))).toBe(
|
||||
'true',
|
||||
)
|
||||
await page.goto('https://docs.example/document/start/option.html')
|
||||
expect(page.url()).toBe('https://docs.example/document/start/option.html')
|
||||
})
|
||||
|
||||
test('a late initial example response cannot overwrite a newer editor Run', async ({
|
||||
page,
|
||||
}) => {
|
||||
let release
|
||||
let started
|
||||
const gate = new Promise((resolve) => {
|
||||
release = resolve
|
||||
})
|
||||
const requested = new Promise((resolve) => {
|
||||
started = resolve
|
||||
})
|
||||
await page.route('https://**/*', route => route.fulfill({ body: '' }))
|
||||
await page.addInitScript(() => {
|
||||
const original = window.fetch
|
||||
window.fetch = async (...args) => {
|
||||
const response = await original(...args)
|
||||
if (String(args[0]).includes('site.slow.js')) {
|
||||
const read = response.text.bind(response)
|
||||
response.text = async () => {
|
||||
const text = await read()
|
||||
window.slowReceived = true
|
||||
return text
|
||||
}
|
||||
}
|
||||
return response
|
||||
}
|
||||
})
|
||||
await page.route('**/assets/example/site.slow.js', async (route) => {
|
||||
started()
|
||||
await gate
|
||||
await route.fulfill({
|
||||
contentType: 'text/javascript',
|
||||
body: 'window.staleRan = true;',
|
||||
})
|
||||
})
|
||||
await page.goto('/?example=site.slow', { waitUntil: 'domcontentloaded' })
|
||||
await requested
|
||||
await page.evaluate(() => {
|
||||
const model = window.monaco.editor
|
||||
.getModels()
|
||||
.find(model => model.getLanguageId() === 'javascript')
|
||||
model.setValue(
|
||||
'window.newerRan = true; var art = new Artplayer({ container: ".artplayer-app", url: "/test/pattern.mp4", muted: true });',
|
||||
)
|
||||
})
|
||||
await page.locator('.run').click()
|
||||
await page.waitForFunction(() => window.art?.isReady)
|
||||
release()
|
||||
await page.waitForFunction(() => window.slowReceived)
|
||||
expect(
|
||||
await page.evaluate(() => ({
|
||||
newer: window.newerRan,
|
||||
stale: !!window.staleRan,
|
||||
ready: window.art.isReady,
|
||||
instances: window.Artplayer.instances.length,
|
||||
})),
|
||||
).toEqual({ newer: true, stale: false, ready: true, instances: 1 })
|
||||
await page.evaluate(() => window.art.destroy(true))
|
||||
})
|
||||
|
||||
test('denied storage keeps the requested documentation page without a redirect loop', async ({
|
||||
page,
|
||||
}) => {
|
||||
const code = fs.readFileSync(
|
||||
'packages/artplayer-vitepress/docs/public/main.js',
|
||||
'utf8',
|
||||
)
|
||||
await page.addInitScript(() => {
|
||||
Object.defineProperty(navigator, 'language', { get: () => 'en-US' })
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
get: () => {
|
||||
throw new DOMException('Blocked', 'SecurityError')
|
||||
},
|
||||
})
|
||||
})
|
||||
let requests = 0
|
||||
await page.route('https://blocked.example/**', (route) => {
|
||||
requests++
|
||||
return route.fulfill({
|
||||
contentType: 'text/html',
|
||||
body: `<html><body>Documentation<script>${code}</script></body></html>`,
|
||||
})
|
||||
})
|
||||
await page.goto('https://blocked.example/document/start/option.html')
|
||||
expect(page.url()).toBe('https://blocked.example/document/start/option.html')
|
||||
expect(requests).toBe(1)
|
||||
expect(await page.evaluate(() => window['run-code-init'])).toBe(true)
|
||||
})
|
||||
@@ -0,0 +1,256 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Use the repository Node regression runner.
|
||||
import test from 'node:test'
|
||||
import vm from 'node:vm'
|
||||
import {
|
||||
createLibraryLoader,
|
||||
exampleSource,
|
||||
parameters,
|
||||
} from '../packages/artplayer-vitepress/browser/loader.ts'
|
||||
import {
|
||||
languageDestination,
|
||||
runCodeUrl,
|
||||
} from '../packages/artplayer-vitepress/browser/navigation.ts'
|
||||
|
||||
const turn = () => new Promise(resolve => setImmediate(resolve))
|
||||
function fixture() {
|
||||
class Element extends EventTarget {
|
||||
removed = false
|
||||
remove() {
|
||||
this.removed = true
|
||||
}
|
||||
|
||||
succeed() {
|
||||
this.onload?.()
|
||||
this.dispatchEvent(new Event('load'))
|
||||
}
|
||||
|
||||
fail() {
|
||||
this.onerror?.()
|
||||
this.dispatchEvent(new Event('error'))
|
||||
}
|
||||
}
|
||||
const nodes = []
|
||||
const head = {
|
||||
appendChild(element) {
|
||||
nodes.push(element)
|
||||
},
|
||||
}
|
||||
const host = {
|
||||
location: { href: 'http://localhost:8082/mobile.html' },
|
||||
define: Object.assign(() => {}, { amd: {} }),
|
||||
document: {
|
||||
head,
|
||||
createElement: () => new Element(),
|
||||
querySelector: () => head,
|
||||
},
|
||||
}
|
||||
return { host, nodes, head }
|
||||
}
|
||||
|
||||
test('fixed old mobile loader reproduces failure and overlapping AMD restoration defects', async () => {
|
||||
const source = execFileSync(
|
||||
'git',
|
||||
[
|
||||
'show',
|
||||
'76499e7a3e033c8350ff495ba8bfff4ca5c08d25:docs/assets/js/mobile.js',
|
||||
],
|
||||
{ encoding: 'utf8' },
|
||||
)
|
||||
const end = source.indexOf(' var _getURLParameters')
|
||||
assert(end > 0)
|
||||
const old = `${source.slice(0, end)} window.legacy = {loadScript, loadLib}; })();`
|
||||
const failed = fixture()
|
||||
vm.runInNewContext(old, {
|
||||
window: failed.host,
|
||||
document: failed.host.document,
|
||||
Artplayer: {},
|
||||
})
|
||||
const failure = failed.host.legacy.loadScript('missing.js')
|
||||
failed.nodes[0].fail()
|
||||
await assert.rejects(failure, /Loading script failed/)
|
||||
assert.equal(failed.host.define, undefined)
|
||||
const parallel = fixture()
|
||||
vm.runInNewContext(old, {
|
||||
window: parallel.host,
|
||||
document: parallel.host.document,
|
||||
Artplayer: {},
|
||||
})
|
||||
const loaded = parallel.host.legacy.loadLib('first.js%0Asecond.js')
|
||||
assert.equal(
|
||||
parallel.nodes.length,
|
||||
2,
|
||||
'Old code starts both scripts before dependencies resolve',
|
||||
)
|
||||
parallel.nodes[0].succeed()
|
||||
parallel.nodes[1].succeed()
|
||||
await loaded
|
||||
assert.equal(
|
||||
parallel.host.define,
|
||||
undefined,
|
||||
'Second request saved the temporarily cleared define',
|
||||
)
|
||||
})
|
||||
|
||||
test('shared loader serializes batches, caches successes, retries failures and restores exact AMD descriptor', async () => {
|
||||
const { host, nodes } = fixture()
|
||||
const original = Object.getOwnPropertyDescriptor(host, 'define')
|
||||
const loader = createLibraryLoader(host)
|
||||
const first = loader.loadLibraries(
|
||||
encodeURIComponent(' ./first.js \n./second.js'),
|
||||
)
|
||||
const second = loader.loadLibraries(
|
||||
encodeURIComponent('./first.js\n./third.css'),
|
||||
)
|
||||
await turn()
|
||||
assert.equal(nodes.length, 1)
|
||||
assert.equal(host.define, undefined)
|
||||
nodes[0].succeed()
|
||||
await turn()
|
||||
assert.equal(nodes.length, 2)
|
||||
const rejected = assert.rejects(first, /Loading script failed/)
|
||||
nodes[1].fail()
|
||||
await rejected
|
||||
await turn()
|
||||
assert.equal(nodes.length, 3)
|
||||
assert(nodes[1].removed)
|
||||
assert.deepEqual(Object.getOwnPropertyDescriptor(host, 'define'), original)
|
||||
nodes[2].succeed()
|
||||
assert.deepEqual(await second, ['./first.js', './third.css'])
|
||||
const retry = loader.loadLibraries('./second.js')
|
||||
await turn()
|
||||
assert.equal(nodes.length, 4)
|
||||
nodes[3].succeed()
|
||||
await retry
|
||||
assert.deepEqual(Object.getOwnPropertyDescriptor(host, 'define'), original)
|
||||
await assert.rejects(loader.loadLibraries('%bad-encoding'), URIError)
|
||||
assert.equal(nodes.length, 4)
|
||||
})
|
||||
|
||||
test('loader restores absence/accessors and synchronous failure, cancels pending work and rejects immutable AMD state', async () => {
|
||||
for (const mode of [
|
||||
'absent',
|
||||
'accessor',
|
||||
'append-failure',
|
||||
'dispose',
|
||||
'immutable',
|
||||
]) {
|
||||
const { host, nodes, head } = fixture()
|
||||
if (mode === 'absent')
|
||||
Reflect.deleteProperty(host, 'define')
|
||||
if (mode === 'accessor') {
|
||||
Object.defineProperty(host, 'define', {
|
||||
get: () => 42,
|
||||
configurable: true,
|
||||
})
|
||||
}
|
||||
if (mode === 'immutable') {
|
||||
Object.defineProperty(host, 'define', {
|
||||
value: 42,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
})
|
||||
}
|
||||
if (mode === 'append-failure') {
|
||||
head.appendChild = () => {
|
||||
throw new Error('append failed')
|
||||
}
|
||||
}
|
||||
const before = Object.getOwnPropertyDescriptor(host, 'define')
|
||||
const loader = createLibraryLoader(host)
|
||||
const pending = loader.loadLibraries('test.js')
|
||||
const fails = ['append-failure', 'dispose', 'immutable'].includes(mode)
|
||||
const rejection = fails ? assert.rejects(pending) : null
|
||||
await turn()
|
||||
if (mode === 'dispose')
|
||||
loader.dispose()
|
||||
else if (!fails)
|
||||
nodes[0].succeed()
|
||||
if (rejection)
|
||||
await rejection
|
||||
else await pending
|
||||
assert.deepEqual(
|
||||
Object.getOwnPropertyDescriptor(host, 'define'),
|
||||
before,
|
||||
mode,
|
||||
)
|
||||
if (mode === 'dispose') {
|
||||
assert(nodes[0].removed)
|
||||
await assert.rejects(loader.loadLibraries('later.js'), /disposed/)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('URL and example contracts preserve literal plus, last duplicate and explicit example priority inputs', async () => {
|
||||
assert.deepEqual(
|
||||
parameters(
|
||||
'http://localhost/?code=a+b%2Bc&libs=first&libs=second&example=mobile#ignored',
|
||||
),
|
||||
{ code: 'a+b%2Bc', libs: 'second', example: 'mobile' },
|
||||
)
|
||||
assert.equal(
|
||||
await exampleSource('mobile', async (url) => {
|
||||
assert.equal(url, './assets/example/mobile.js')
|
||||
return new Response('var art = 1')
|
||||
}),
|
||||
'var art = 1',
|
||||
)
|
||||
await assert.rejects(
|
||||
exampleSource(
|
||||
'missing',
|
||||
async () => new Response('404 HTML', { status: 404 }),
|
||||
),
|
||||
/404/,
|
||||
)
|
||||
const libs = 'https://cdn.example/lib.js?a=1&b=two\n./theme.css'
|
||||
const code = 'var x = "a+b&你好";'
|
||||
for (const host of ['localhost', '127.0.0.1', '[::1]']) {
|
||||
const url = new URL(runCodeUrl({ hostname: host }, libs, code))
|
||||
assert.equal(url.host, `${host}:8082`)
|
||||
assert.equal(url.searchParams.get('libs'), libs)
|
||||
assert.equal(url.searchParams.get('code'), code)
|
||||
}
|
||||
assert.equal(
|
||||
new URL(runCodeUrl({ hostname: 'artplayer.org' }, '', code)).origin,
|
||||
'https://artplayer.org',
|
||||
)
|
||||
})
|
||||
|
||||
test('language routing preserves translated deep links and explicit choices without redirecting local previews', () => {
|
||||
const paths = ['', 'index.html', 'start/option.html']
|
||||
const choose = (url, lang = 'en-US', initialized = false) =>
|
||||
languageDestination(url, lang, initialized, paths)
|
||||
assert.deepEqual(
|
||||
choose('https://artplayer.org/document/start/option.html?x=1#url'),
|
||||
{
|
||||
remember: true,
|
||||
target: 'https://artplayer.org/document/en/start/option.html?x=1#url',
|
||||
},
|
||||
)
|
||||
assert.deepEqual(
|
||||
choose('https://artplayer.org/document/plugin/danmuku.html#x'),
|
||||
{ remember: true, target: 'https://artplayer.org/document/en/' },
|
||||
)
|
||||
assert.deepEqual(
|
||||
choose('https://artplayer.org/document/en/start/option.html'),
|
||||
{ remember: true, target: null },
|
||||
)
|
||||
for (const host of ['localhost', '127.0.0.1', '[::1]']) {
|
||||
assert.deepEqual(choose(`http://${host}:5173/document/`), {
|
||||
remember: false,
|
||||
target: null,
|
||||
})
|
||||
}
|
||||
for (const [language, initialized] of [
|
||||
['zh-CN', false],
|
||||
['ZH-tw', false],
|
||||
['en', true],
|
||||
['', false],
|
||||
]) {
|
||||
assert.deepEqual(
|
||||
choose('https://artplayer.org/document/', language, initialized),
|
||||
{ remember: false, target: null },
|
||||
)
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user