Fix emitted frontend boot and verify bundled runtime initialization

This commit is contained in:
2026-10-12 18:07:01 +08:00
parent f04600c37b
commit 6735999e63
3 changed files with 43 additions and 7 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ Tags: asciicast, asciiplayer, asciinema, terminal, block
Requires at least: 6.3 Requires at least: 6.3
Requires PHP: 7.4 Requires PHP: 7.4
Stable tag: 1.1.0 Stable tag: 1.1.0
License: GPLv3 License: GPL-3.0-only
License URI: https://www.gnu.org/licenses/gpl-3.0.html License URI: https://www.gnu.org/licenses/gpl-3.0.html
Play terminal recordings with a native block, real editor playback previews, and compatible shortcodes. Play terminal recordings with a native block, real editor playback previews, and compatible shortcodes.
+37 -1
View File
@@ -1,5 +1,7 @@
import { readFile } from 'node:fs/promises'; import { readFile } from 'node:fs/promises';
import { createHash } from 'node:crypto'; import { createHash } from 'node:crypto';
import { runInNewContext } from 'node:vm';
import assert from 'node:assert/strict';
const hash = (data) => createHash('sha256').update(data).digest('hex'); const hash = (data) => createHash('sha256').update(data).digest('hex');
const asset = await readFile('build/index.asset.php', 'utf8'); const asset = await readFile('build/index.asset.php', 'utf8');
if (asset.includes('react-jsx-runtime')) throw new Error('Automatic JSX runtime would break the WordPress 6.3 baseline.'); if (asset.includes('react-jsx-runtime')) throw new Error('Automatic JSX runtime would break the WordPress 6.3 baseline.');
@@ -14,4 +16,38 @@ for (const [destination, original] of [
} }
const metadata = JSON.parse(await readFile('build/block.json', 'utf8')); const metadata = JSON.parse(await readFile('build/block.json', 'utf8'));
if (metadata.apiVersion !== 3 || metadata.name !== 'lamgc/asciiplayer') throw new Error('Unexpected block metadata.'); if (metadata.apiVersion !== 3 || metadata.name !== 'lamgc/asciiplayer') throw new Error('Unexpected block metadata.');
console.log('Verified WordPress 6.3-compatible JSX dependencies, block metadata, and exact local vendor files.'); // Execute the emitted bundle too: source-only tests miss minifier scope errors.
// In particular, never shadow the global document used by mountPlayers' default.
const view = await readFile('build/view.js', 'utf8');
for (const readyState of ['loading', 'complete']) {
let mounts = 0;
let onReady;
const container = {
dataset: { apSrc: '/recording.cast', apOptions: '{"preload":false}' },
};
const document = {
readyState,
body: { classList: { contains: () => false } },
querySelector: () => null,
querySelectorAll: () => [container],
addEventListener(event, callback) {
assert.equal(event, 'DOMContentLoaded');
onReady = callback;
},
defaultView: { AsciinemaPlayer: { create(source, target) {
assert.equal(source, '/recording.cast');
assert.equal(target, container);
mounts++;
return { addEventListener() {} };
} } },
};
runInNewContext(view, { document, window: { wp: { i18n: { __: (text) => text } } }, URL });
if (readyState === 'loading') {
assert.equal(mounts, 0);
onReady();
onReady(); // A repeated boot must not mount the same container twice.
}
assert.equal(mounts, 1, `Frontend boot failed at readyState=${readyState}`);
assert.equal(container.dataset.apStatus, 'idle');
}
console.log('Verified WordPress 6.3 dependencies, block metadata, exact vendor files, and emitted frontend boot.');
+5 -5
View File
@@ -15,9 +15,9 @@ function showError( container ) {
/** One explicit instance per front-end container; never scan an editor canvas. */ /** One explicit instance per front-end container; never scan an editor canvas. */
export function mountPlayers( root = document ) { export function mountPlayers( root = document ) {
const document = root.ownerDocument || root; const ownerDocument = root.ownerDocument || root;
if ( document.body?.classList.contains( 'wp-admin' ) || document.querySelector( '.editor-styles-wrapper, .block-editor-block-list__layout' ) ) return; if ( ownerDocument.body?.classList.contains( 'wp-admin' ) || ownerDocument.querySelector( '.editor-styles-wrapper, .block-editor-block-list__layout' ) ) return;
const api = document.defaultView?.AsciinemaPlayer; const api = ownerDocument.defaultView?.AsciinemaPlayer;
if ( typeof api?.create !== 'function' ) return; if ( typeof api?.create !== 'function' ) return;
root.querySelectorAll( '.asciiplayer-container' ).forEach( ( container ) => { root.querySelectorAll( '.asciiplayer-container' ).forEach( ( container ) => {
if ( instances.has( container ) ) return; if ( instances.has( container ) ) return;
@@ -45,10 +45,10 @@ export function mountPlayers( root = document ) {
// by its parsed duration rather than merely by a mounted DOM wrapper. // by its parsed duration rather than merely by a mounted DOM wrapper.
if ( options.preload || options.autoPlay || options.poster?.startsWith( 'npt:' ) ) { if ( options.preload || options.autoPlay || options.poster?.startsWith( 'npt:' ) ) {
let ticks = 0; let ticks = 0;
const timer = document.defaultView.setInterval( () => { const timer = ownerDocument.defaultView.setInterval( () => {
const duration = player.getDuration(); const duration = player.getDuration();
if ( typeof duration === 'number' && Number.isFinite( duration ) ) container.dataset.apStatus = 'ready'; if ( typeof duration === 'number' && Number.isFinite( duration ) ) container.dataset.apStatus = 'ready';
if ( ! container.isConnected || container.dataset.apStatus !== 'loading' || ++ticks > 200 ) document.defaultView.clearInterval( timer ); if ( ! container.isConnected || container.dataset.apStatus !== 'loading' || ++ticks > 200 ) ownerDocument.defaultView.clearInterval( timer );
}, 150 ); }, 150 );
} else { } else {
container.dataset.apStatus = 'idle'; container.dataset.apStatus = 'idle';