6dfd5203d7
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m5s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m34s
The es2022 build target shipped optional chaining / nullish coalescing verbatim (esbuild does not down-level syntax below the configured target), so an old Android System WebView — captured as Chromium 66 in the Telegram/VK in-app WebView on Android 9, via the boot diagnostic — rejected the bundle with "Uncaught SyntaxError: Unexpected token ?" at main-*.js:2, the module never ran (no .app-shell, #app empty) and the SPA showed a white screen. Firefox/Gecko on the same device, which ships its own current engine, rendered the SPA fine. Lower build.target to es2019 so esbuild down-levels the es2020+ syntax the old engine cannot parse (?., ??, private fields, static blocks, numeric separators). Verified the emitted main bundle no longer contains ?./?? and the mock e2e (Chromium + WebKit) still passes. The boot-diagnostic JS-SYNTAX header is relabelled accordingly. esbuild lowers syntax only, not runtime APIs; the app's own production source uses none beyond this floor (mock-only helpers are tree-shaken). A graceful "update your WebView" fallback and any runtime-API polyfills follow once the on-device re-test confirms the parse fix (and @vitejs/plugin-legacy stays in reserve for even older engines).
321 lines
16 KiB
HTML
321 lines
16 KiB
HTML
<!doctype html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<!-- The SPA shell is an empty client-rendered document behind the public landing — keep it
|
|
out of search indexes (robots.txt deliberately does NOT disallow /app/, so crawlers can
|
|
reach this tag). -->
|
|
<meta name="robots" content="noindex" />
|
|
<!-- The Telegram Mini App SDK (window.Telegram.WebApp) is deliberately NOT loaded here: a
|
|
render-blocking <script> to telegram.org hangs the whole page on a network that blocks
|
|
telegram.org (common where Telegram itself reaches users only over a proxy), stranding even
|
|
the launch-diagnostic screen. The app loads it dynamically, with a timeout, only on a
|
|
Telegram entry — see lib/telegram.ts loadTelegramSDK and lib/app.svelte.ts bootstrap. -->
|
|
<!-- user-scalable=no: the board owns zoom; we do not want the browser's pinch
|
|
to fight our two-state zoom. viewport-fit=cover for native (Capacitor). -->
|
|
<meta
|
|
name="viewport"
|
|
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
|
|
/>
|
|
<title>Эрудит (Скрэббл)</title>
|
|
<!-- Relative icon hrefs: the gateway serves the same build under /app/, /telegram/ and /vk/. -->
|
|
<link rel="icon" href="favicon.ico" sizes="32x32" />
|
|
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
|
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
|
|
</head>
|
|
<body>
|
|
<!-- BOOT-DIAG:START — TEMPORARY on-device boot diagnostic (test contour only).
|
|
|
|
A classic (non-module) ES5 <script> placed BEFORE the ES2022 module bundle. It runs and
|
|
paints even when the bundle fails to parse on an old Android System WebView (the case we
|
|
are chasing: Firefox/Gecko renders the SPA on the same device, the in-app WebView shows a
|
|
white screen). It installs error capture first, so a module SyntaxError / load failure is
|
|
printed here, then reports the engine (UA + Chromium version), the JS/Web feature support
|
|
the bundle needs, and whether the module ran / Svelte mounted.
|
|
|
|
vite.config.ts (strip-boot-diag) removes this whole block from every non-production build,
|
|
so the mock e2e overlay stays clear. Delete this block AND that plugin after the diagnosis
|
|
— it must never reach master / production. -->
|
|
<script>
|
|
(function () {
|
|
'use strict';
|
|
var t0 = Date.now();
|
|
var nav = navigator;
|
|
|
|
// ---- overlay shell ---------------------------------------------------------------------
|
|
var root = document.createElement('div');
|
|
root.id = 'bootdiag';
|
|
root.setAttribute(
|
|
'style',
|
|
'position:fixed;left:0;top:0;right:0;bottom:0;z-index:2147483647;overflow:auto;' +
|
|
'-webkit-overflow-scrolling:touch;background:#0b0b0e;color:#e6e6e6;padding:12px;' +
|
|
'font:12.5px/1.45 ui-monospace,Menlo,Consolas,monospace;box-sizing:border-box;'
|
|
);
|
|
function el(tag, style, text) {
|
|
var e = document.createElement(tag);
|
|
if (style) e.setAttribute('style', style);
|
|
if (text != null) e.appendChild(document.createTextNode(text));
|
|
return e;
|
|
}
|
|
root.appendChild(el('div', 'font-size:15px;font-weight:700;margin-bottom:2px;', 'BOOT DIAGNOSTIC'));
|
|
root.appendChild(
|
|
el('div', 'color:#7ee081;margin-bottom:10px;', 'HTML parsed and classic JS is running (you can read this).')
|
|
);
|
|
|
|
var btnStyle =
|
|
'display:inline-block;margin:0 8px 12px 0;padding:10px 16px;border:0;border-radius:6px;' +
|
|
'font:600 13px/1 inherit;color:#0b0b0e;background:#7ee081;';
|
|
var hideBtn = el('button', btnStyle, 'Hide / continue to app');
|
|
var copyBtn = el('button', btnStyle.replace('#7ee081', '#8ab4ff'), 'Copy report');
|
|
var bar = el('div', null);
|
|
bar.appendChild(hideBtn);
|
|
bar.appendChild(copyBtn);
|
|
root.appendChild(bar);
|
|
|
|
function section(title) {
|
|
root.appendChild(el('div', 'margin:12px 0 4px;font-weight:700;color:#9aa0aa;', title));
|
|
var pre = el('pre', 'margin:0;white-space:pre-wrap;word-break:break-word;');
|
|
root.appendChild(pre);
|
|
return pre;
|
|
}
|
|
|
|
// ---- error capture (installed before the module runs) ----------------------------------
|
|
var errBox = el('div', 'margin:12px 0 4px;font-weight:700;color:#ff8080;', 'ERRORS (from the module bundle)');
|
|
errBox.style.display = 'none';
|
|
var errPre = el('pre', 'margin:0;white-space:pre-wrap;word-break:break-word;color:#ffb3b3;');
|
|
errPre.style.display = 'none';
|
|
function stamp() {
|
|
return '[t+' + ((Date.now() - t0) / 1000).toFixed(2) + 's] ';
|
|
}
|
|
function logErr(prefix, msg) {
|
|
errBox.style.display = 'block';
|
|
errPre.style.display = 'block';
|
|
errPre.appendChild(document.createTextNode((errPre.firstChild ? '\n' : '') + stamp() + prefix + ': ' + msg));
|
|
}
|
|
window.onerror = function (message, source, lineno, colno, error) {
|
|
logErr(
|
|
'window.onerror',
|
|
String(message) +
|
|
(source ? ' @ ' + source + ':' + lineno + ':' + colno : '') +
|
|
(error && error.stack ? '\n' + error.stack : '')
|
|
);
|
|
return false;
|
|
};
|
|
window.addEventListener(
|
|
'error',
|
|
function (e) {
|
|
var tgt = e && e.target;
|
|
if (tgt && tgt !== window && (tgt.src || tgt.href)) {
|
|
logErr('resource load error', (tgt.tagName || '?') + ' ' + (tgt.src || tgt.href));
|
|
} else if (e && e.message) {
|
|
logErr('error event', e.message + (e.filename ? ' @ ' + e.filename + ':' + e.lineno : ''));
|
|
}
|
|
},
|
|
true
|
|
);
|
|
window.addEventListener('unhandledrejection', function (e) {
|
|
var r = e && e.reason;
|
|
logErr('unhandledrejection', r && (r.stack || r.message) ? r.stack || r.message : String(r));
|
|
});
|
|
|
|
// ---- environment -----------------------------------------------------------------------
|
|
var ua = nav.userAgent || '(no userAgent)';
|
|
var cm = /Chrom(?:e|ium)\/(\d+)/.exec(ua);
|
|
var chromeMajor = cm ? parseInt(cm[1], 10) : null;
|
|
var isWV = /;\s*wv\)/.test(ua) || /\bwv\b/.test(ua);
|
|
var envPre = section('ENGINE');
|
|
var envLines = [
|
|
'userAgent : ' + ua,
|
|
'Chromium : ' + (chromeMajor != null ? chromeMajor : 'not a Chromium UA') + (isWV ? ' (Android WebView "wv")' : ''),
|
|
'vendor : ' + (nav.vendor || '(none)'),
|
|
'url : ' + location.href,
|
|
'viewport : ' + window.innerWidth + 'x' + window.innerHeight + ' @dpr ' + (window.devicePixelRatio || 1),
|
|
'cookies : ' + nav.cookieEnabled + ' online: ' + nav.onLine + ' lang: ' + (nav.language || '?'),
|
|
'readyState: ' + document.readyState,
|
|
];
|
|
envPre.appendChild(document.createTextNode(envLines.join('\n')));
|
|
|
|
// ---- JS syntax probes (parsed via new Function so a probe never crashes the engine) -----
|
|
var evalWorks = true;
|
|
try {
|
|
new Function('return 1');
|
|
} catch (e) {
|
|
evalWorks = false;
|
|
}
|
|
function synOk(body) {
|
|
try {
|
|
new Function(body);
|
|
return true;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
// [label, function-body-to-parse, Chrome version it landed in]
|
|
var syn = [
|
|
['optional chaining a?.b', 'var o={};return o?.b', 80],
|
|
['nullish coalescing a ?? b', 'return (null ?? 1)', 80],
|
|
['nullish assignment a ??= b', 'var a=null;a??=2;return a', 85],
|
|
['logical-or assignment a ||= b', 'var a=0;a||=2;return a', 85],
|
|
['private class field #x', 'class C{#v=1;r(){return this.#v}}return new C().r()', 74],
|
|
['static class block', 'class C{static v;static{C.v=1}}return C.v', 94],
|
|
['object spread {...o}', 'var o={a:1};return {b:1,...o}', 60],
|
|
['numeric separator 1_000', 'return 1_000', 75],
|
|
['dynamic import()', 'return import("")', 63],
|
|
];
|
|
var synPre = section('JS SYNTAX (engine feature ceiling — first NO dates the engine)');
|
|
function mark(ok) {
|
|
return ok ? 'OK ' : 'NO ';
|
|
}
|
|
var synFail = 0;
|
|
var baselineBroken = false;
|
|
if (!evalWorks) {
|
|
synPre.appendChild(document.createTextNode('eval / new Function is blocked (CSP?) — syntax probes skipped; rely on ENGINE + APIS.'));
|
|
} else {
|
|
var synText = [];
|
|
for (var i = 0; i < syn.length; i++) {
|
|
var ok = synOk(syn[i][1]);
|
|
if (!ok) {
|
|
synFail++;
|
|
if (syn[i][2] <= 80) baselineBroken = true; // ?. / ?? missing ⇒ the bundle cannot parse
|
|
}
|
|
synText.push(mark(ok) + syn[i][0] + ' (Chrome ' + syn[i][2] + ')');
|
|
}
|
|
synPre.appendChild(document.createTextNode(synText.join('\n')));
|
|
}
|
|
|
|
// ---- JS / Web API probes ---------------------------------------------------------------
|
|
function has(fn) {
|
|
try {
|
|
return !!fn();
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
var api = [
|
|
['structuredClone', function () { return typeof structuredClone === 'function'; }, 98],
|
|
['Array.prototype.at', function () { return typeof [].at === 'function'; }, 92],
|
|
['Array.prototype.findLast', function () { return typeof [].findLast === 'function'; }, 97],
|
|
['Object.hasOwn', function () { return typeof Object.hasOwn === 'function'; }, 93],
|
|
['Object.fromEntries', function () { return typeof Object.fromEntries === 'function'; }, 73],
|
|
['Promise.allSettled', function () { return !!Promise.allSettled; }, 76],
|
|
['Promise.any', function () { return !!Promise.any; }, 85],
|
|
['globalThis', function () { return typeof globalThis !== 'undefined'; }, 71],
|
|
['BigInt', function () { return typeof BigInt !== 'undefined'; }, 67],
|
|
['Proxy (Svelte 5 runes need it)', function () { return typeof Proxy !== 'undefined'; }, 49],
|
|
['WeakRef', function () { return typeof WeakRef !== 'undefined'; }, 84],
|
|
['queueMicrotask', function () { return typeof queueMicrotask === 'function'; }, 71],
|
|
['crypto.subtle', function () { return !!(window.crypto && window.crypto.subtle); }, 37],
|
|
['CSS.supports', function () { return !!(window.CSS && window.CSS.supports); }, 28],
|
|
['ResizeObserver', function () { return typeof ResizeObserver !== 'undefined'; }, 64],
|
|
['IntersectionObserver', function () { return typeof IntersectionObserver !== 'undefined'; }, 51],
|
|
['fetch', function () { return typeof fetch === 'function'; }, 42],
|
|
['AbortController', function () { return typeof AbortController !== 'undefined'; }, 66],
|
|
['Intl.Segmenter', function () { return !!(window.Intl && Intl.Segmenter); }, 87],
|
|
['localStorage (may be blocked in WebView)', function () {
|
|
var k = '__bd';
|
|
localStorage.setItem(k, '1');
|
|
localStorage.removeItem(k);
|
|
return true;
|
|
}, 4],
|
|
];
|
|
var apiPre = section('JS / WEB APIS');
|
|
var apiFail = 0;
|
|
var apiText = [];
|
|
for (var j = 0; j < api.length; j++) {
|
|
var okj = has(api[j][1]);
|
|
if (!okj) apiFail++;
|
|
apiText.push(mark(okj) + api[j][0] + ' (Chrome ' + api[j][2] + ')');
|
|
}
|
|
apiPre.appendChild(document.createTextNode(apiText.join('\n')));
|
|
|
|
// ---- verdict ---------------------------------------------------------------------------
|
|
var verdict = section('VERDICT');
|
|
var vLines = [];
|
|
if (baselineBroken) {
|
|
vLines.push('LIKELY CAUSE: engine lacks ES2020 optional-chaining / nullish coalescing —');
|
|
vLines.push('the es2022 module bundle cannot be parsed, so the SPA never mounts (white screen).');
|
|
} else if (synFail > 0 || apiFail > 0) {
|
|
vLines.push(synFail + ' syntax + ' + apiFail + ' API features missing. If the module error below');
|
|
vLines.push('names one of them, that is the blocker; otherwise the engine is likely new enough.');
|
|
} else {
|
|
vLines.push('All probed features supported — engine is not the blocker.');
|
|
vLines.push('If the SPA still fails, read the module ERRORS below and the MOUNT check.');
|
|
}
|
|
verdict.appendChild(document.createTextNode(vLines.join('\n')));
|
|
|
|
// ---- module / mount check (no coupling: reads the DOM the bundle would touch) -----------
|
|
var mountPre = section('SPA MOUNT CHECK (does the ES2022 module actually run + mount?)');
|
|
function mountLine() {
|
|
var shell = document.documentElement.className.indexOf('app-shell') >= 0; // main.ts line 8
|
|
var appEl = document.getElementById('app');
|
|
var n = appEl ? appEl.childElementCount : -1;
|
|
var verdictTxt = n > 0 ? 'SPA mounted' : shell ? 'module RAN but Svelte did NOT mount' : 'module did NOT run (parse/load failure)';
|
|
return stamp() + '<html.app-shell>=' + (shell ? 'YES' : 'no') + ' #app children=' + n + ' → ' + verdictTxt;
|
|
}
|
|
function addMount() {
|
|
mountPre.appendChild(document.createTextNode((mountPre.firstChild ? '\n' : '') + mountLine()));
|
|
}
|
|
// First reading at DOMContentLoaded: deferred module scripts (the ES2022 bundle) have
|
|
// executed by then, so #app is either mounted or provably not — a reading taken any earlier
|
|
// races the parser (the #app div is emitted after this script) and always reads "not run".
|
|
// The later readings catch a slow / async mount and show whether it held.
|
|
function scheduleMounts() {
|
|
addMount();
|
|
setTimeout(addMount, 3000);
|
|
setTimeout(addMount, 8000);
|
|
}
|
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', scheduleMounts);
|
|
else scheduleMounts();
|
|
|
|
// ---- errors area goes last -------------------------------------------------------------
|
|
root.appendChild(errBox);
|
|
root.appendChild(errPre);
|
|
|
|
// ---- buttons ---------------------------------------------------------------------------
|
|
hideBtn.onclick = function () {
|
|
root.style.display = 'none';
|
|
};
|
|
function fullReport() {
|
|
return (
|
|
envLines.join('\n') +
|
|
'\n\n=== VERDICT ===\n' + vLines.join('\n') +
|
|
'\n\n=== JS SYNTAX ===\n' + (evalWorks ? synPre.textContent : '(eval blocked)') +
|
|
'\n\n=== JS/WEB APIS ===\n' + apiText.join('\n') +
|
|
'\n\n=== SPA MOUNT ===\n' + mountPre.textContent +
|
|
'\n\n=== ERRORS ===\n' + (errPre.textContent || '(none)')
|
|
);
|
|
}
|
|
copyBtn.onclick = function () {
|
|
var txt = fullReport();
|
|
try {
|
|
if (nav.clipboard && nav.clipboard.writeText) {
|
|
nav.clipboard.writeText(txt);
|
|
copyBtn.firstChild.nodeValue = 'Copied';
|
|
return;
|
|
}
|
|
} catch (e) {}
|
|
try {
|
|
var ta = document.createElement('textarea');
|
|
ta.value = txt;
|
|
ta.setAttribute('style', 'position:fixed;left:-9999px;top:0;');
|
|
document.body.appendChild(ta);
|
|
ta.select();
|
|
document.execCommand('copy');
|
|
document.body.removeChild(ta);
|
|
copyBtn.firstChild.nodeValue = 'Copied';
|
|
} catch (e2) {
|
|
copyBtn.firstChild.nodeValue = 'Copy failed — long-press the text';
|
|
}
|
|
};
|
|
|
|
// Attach as the first body child, above #app, so it overlays the (possibly blank) SPA.
|
|
if (document.body) document.body.insertBefore(root, document.body.firstChild);
|
|
else document.addEventListener('DOMContentLoaded', function () { document.body.insertBefore(root, document.body.firstChild); });
|
|
})();
|
|
</script>
|
|
<!-- BOOT-DIAG:END -->
|
|
<div id="app"></div>
|
|
<script type="module" src="/src/main.ts"></script>
|
|
</body>
|
|
</html>
|