fix(docs): [SITE-LOAD-01] serialize example loading and preserve navigation

This commit is contained in:
Harvey Zhao committed 2026-09-14 07:52:32 +08:00
1 parent 76499e7a3e
commit 647f3e7f12
29 files changed
+1977 -256

No files matched your search

+39 -109
View File
@@ -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)
})
+135
View File
@@ -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
View File
@@ -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));
})();
+1
View File
@@ -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
View File
@@ -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": {
+12 -6
View File
@@ -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"]);
})();
+27 -6
View File
@@ -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、任务、当前清单和独立变更记录。
+2
View File
@@ -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
View File
@@ -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)
+16
View File
@@ -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 命令/导入路径保留。
+1 -1
View File
@@ -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,
+4
View File
@@ -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
View File
@@ -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": []
},
{
+35
View File
@@ -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}`)
+1
View File
@@ -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"
+4
View File
@@ -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
+6
View File
@@ -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
+1 -1
View File
@@ -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}`))]))) })
+329
View File
@@ -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&amp;b=2"><span id="run-inner">Run</span></div><div><pre><code>var x = "你好+a&amp;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)
})
+256
View File
@@ -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 },
)
}
})