// Live check of string packs on signed-in x.com, in the running release app (started with // WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=). Plain CDP: one tab of its // own, opened and closed through the HTTP endpoint, and one websocket to that tab only. // // node scripts/strings-live-check.mjs [outDir] [public profile handle] // // It works in a background tab of its own in the app's signed-in profile and only reads: no like, // follow, post, message or setting; the only clicks are none at all (pages are loaded by URL). The // freshly built X runtime (src-tauri/x-runtime.js, `node scripts/build-inject.mjs`) is injected // at document start the way the app injects it, and the test pack is put in the check's own tab's // session storage (`wayBetter.stringPacks`: no other tab sees it) and removed at the end. // // Checks: X's own text first (baseline); with the pack, strings change, nothing of the original // flashes, a second registration and a reload change nothing further; without the pack again, the // page's own text is back exactly. Screenshots are of the public profile page only, clipped to the // navigation labels and the profile column (never the account button, never the home timeline). import { execFileSync } from 'node:child_process'; import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; const root = join(dirname(fileURLToPath(import.meta.url)), '..'); const [port = '9477', out = join(root, '../../verification/string-packs'), handle = 'X'] = process.argv.slice(2); const runtime = readFileSync(join(root, 'src-tauri/x-runtime.js'), 'utf8'); const pack = JSON.parse(readFileSync(join(root, 'strings/packs/testish.strings.json'), 'utf8')); const inventory = JSON.parse(readFileSync(join(root, 'strings/x.web.inventory.json'), 'utf8')); const { sliceInventory } = await import('../../../waybetter/packages/runtime/src/stringPacks.ts'); /** What the page gets: the pack and the slice of the inventory its keys need. */ const delivery = { inventory: sliceInventory(inventory, Object.keys(pack.strings)), packs: [pack] }; const KEY = 'wayBetter.stringPacks'; const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); const lines = []; const log = line => { lines.push(line); console.log(line); }; const check = (name, ok, detail = '') => log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` (${detail})` : ''}`); const alive = () => execFileSync('powershell', ['-NoProfile', '-Command', '(Get-Process better-twttr -ErrorAction SilentlyContinue | Measure-Object).Count']).toString().trim(); // ---- CDP: one tab of our own (HTTP endpoint) and one websocket to that tab only --------------- const endpoint = `http://127.0.0.1:${port}`; const pages = async () => (await (await fetch(`${endpoint}/json/list`)).json()).filter(target => target.type === 'page').length; const pagesBefore = await pages(); const own = await (await fetch(`${endpoint}/json/new?about:blank`, { method: 'PUT' })).json(); const ws = new WebSocket(own.webSocketDebuggerUrl); await new Promise((resolve, reject) => { ws.onopen = resolve; ws.onerror = reject; }); let seq = 0; const pending = new Map(); const cdp = (method, params = {}) => new Promise((resolve, reject) => { const id = ++seq; const timer = setTimeout(() => { pending.delete(id); reject(new Error(`${method}: no answer in 60 s`)); }, 60000); pending.set(id, message => { clearTimeout(timer); if (message.error) reject(new Error(`${method}: ${message.error.message}`)); else resolve(message.result); }); ws.send(JSON.stringify({ id, method, params })); }); ws.onmessage = event => { const message = JSON.parse(event.data); if (message.id) { pending.get(message.id)?.(message); pending.delete(message.id); } // A dialog this check caused must not hang the shared app. else if (message.method === 'Page.javascriptDialogOpening') cdp('Page.handleJavaScriptDialog', { accept: false }).catch(() => {}); }; const evaluate = async expression => { const result = await cdp('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true, timeout: 45000 }); if (result.exceptionDetails) throw new Error(result.exceptionDetails.exception?.description ?? result.exceptionDetails.text); return result.result?.value; }; // ---- page helpers ------------------------------------------------------------------------- /** Records every short text the page ever shows, with the time it was first seen. */ const WATCHER = `(() => { if (window.top !== window || window.__wbSeen) return; const seen = window.__wbSeen = new Map(); const note = value => { if (value && value.length <= 60 && seen.size < 6000 && !seen.has(value)) seen.set(value, Math.round(performance.now())); }; new MutationObserver(records => { for (const record of records) { if (record.type === 'characterData') note(record.target.nodeValue); for (const node of record.addedNodes) { if (node.nodeType === 3) note(node.nodeValue); else if (node.nodeType === 1) { const walker = document.createTreeWalker(node, 4); while (walker.nextNode()) note(walker.currentNode.nodeValue); } } } }).observe(document, { subtree: true, childList: true, characterData: true }); })();`; /** The page's own wording: text and labels of the navigation and of the main column outside posts. */ const CHROME = `(() => { const out = []; // Not X's wording: posts, the account button, and what the app's own runtime adds or rewords (its // navigation item, the "wording" option's buttons). const skip = '[data-testid="SideNav_AccountSwitcher_Button"], [data-testid="cellInnerDiv"], article, script, style, [data-testid="AppTabBar_BetterTwttr"], [data-bt-retext], [data-testid="SideNav_NewTweet_Button"], button[data-testid^="tweetButton"]'; for (const root of [document.querySelector('header[role="banner"]'), document.querySelector('[data-testid="primaryColumn"]')]) { if (!root) { out.push(''); continue; } const walker = document.createTreeWalker(root, 4); while (walker.nextNode()) { const node = walker.currentNode; const value = node.nodeValue.trim(); if (value && !node.parentElement.closest(skip)) out.push('t:' + value); } for (const element of root.querySelectorAll('[aria-label], [title], [placeholder]')) { if (element.closest(skip)) continue; for (const name of ['aria-label', 'title', 'placeholder']) if (element.hasAttribute(name)) out.push(name[0] + ':' + element.getAttribute(name)); } } return out; })()`; /** Loaded: the navigation, the main column and its first posts are there, and the wording stopped changing. */ const ready = async () => { let last = ''; let same = 0; for (let i = 0; i < 90; i += 1) { await wait(700); const ok = await evaluate(`!!document.querySelector('header[role="banner"] nav a') && !!document.querySelector('[data-testid="primaryColumn"] [role="tablist"], [data-testid="primaryColumn"] [data-testid="UserName"]') && !!document.querySelector('[data-testid="primaryColumn"] [data-testid="cellInnerDiv"] article')`).catch(() => false); if (!ok) { same = 0; continue; } const now = JSON.stringify(await evaluate(CHROME)).replace(/[d.,]+[KMB]?/g, '#'); same = now === last ? same + 1 : 0; last = now; if (same >= 2) return true; } return false; }; const open = async path => { await cdp('Page.navigate', { url: `https://x.com${path}` }); return ready(); }; const scripts = []; const inject = async sources => { for (const identifier of scripts.splice(0)) await cdp('Page.removeScriptToEvaluateOnNewDocument', { identifier }); for (const source of sources) scripts.push((await cdp('Page.addScriptToEvaluateOnNewDocument', { source })).identifier); }; const storePack = on => evaluate(on ? `sessionStorage.setItem(${JSON.stringify(KEY)}, ${JSON.stringify(JSON.stringify(delivery))})` : `sessionStorage.removeItem(${JSON.stringify(KEY)})`); const report = () => evaluate('window.wayBetter?.stringPacks?.report?.() ?? null'); /** Text diff of two chrome lists as multisets: what only a has, what only b has. */ const differ = (a, b) => { const count = list => list.reduce((map, item) => map.set(item, (map.get(item) ?? 0) + 1), new Map()); const [ca, cb] = [count(a), count(b)]; const only = (x, y) => [...x].flatMap(([item, n]) => (n > (y.get(item) ?? 0) ? [item] : [])); return { onlyA: only(ca, cb), onlyB: only(cb, ca) }; }; const numbersAside = list => list.map(item => item.replace(/[\d.,]+[KMB]?/g, '#')); mkdirSync(out, { recursive: true }); const summary = { when: new Date().toISOString(), build: null, language: null, pages: {} }; try { await cdp('Page.enable'); await cdp('Emulation.setDeviceMetricsOverride', { width: 1360, height: 900, deviceScaleFactor: 1, mobile: false }); await open('/home'); await storePack(false); const pagesOf = [['home', '/home'], ['profile', `/${handle}`]]; // 1. X's own text: the runtime injected, no pack stored. await inject([WATCHER, runtime]); const baseline = {}; for (const [name, path] of pagesOf) { check(`${name}: page loaded (no pack)`, await open(path)); baseline[name] = await evaluate(CHROME); const state = await evaluate(`({ lang: document.documentElement.lang, signedIn: !!document.querySelector('[data-testid="SideNav_AccountSwitcher_Button"]'), runtime: !!window.wayBetter, report: window.wayBetter?.stringPacks?.report?.() ?? null, pushIsPlain: !Object.getOwnPropertyDescriptor(window.webpackChunk_twitter_responsive_web ?? [], 'push')?.get, main: [...document.scripts].map(s => s.src).find(s => /\\/main\\./.test(s))?.split('/').pop() })`); summary.language = state.lang; summary.build = state.main; check(`${name}: signed in, runtime present, nothing hooked without a pack`, state.signedIn && state.runtime && Array.isArray(state.report) && state.report.length === 0 && state.pushIsPlain, `lang ${state.lang}, ${baseline[name].length} chrome strings`); } // 2. With the pack. await storePack(true); const withPack = {}; for (const [name, path] of pagesOf) { check(`${name}: page loaded (pack stored)`, await open(path)); withPack[name] = await evaluate(CHROME); const [state] = (await report()) ?? []; const diff = differ(baseline[name], withPack[name]); const changedFrom = diff.onlyA.filter(item => !/\d/.test(item)); const packValues = new Set(Object.values(pack.strings).filter(value => !value.includes('{'))); const changedTo = diff.onlyB.filter(item => packValues.has(item.slice(2)) || /postle|Arrived/.test(item)); check(`${name}: hook ok, pack applied`, state?.status === 'ok' && state.engine.applied > 40, state ? `status ${state.status}, language ${state.languages.join('+')}, files ${JSON.stringify(state.seen)}, keys seen ${state.engine.seen}, applied ${state.engine.applied}, unlisted ${state.engine.unlisted.length}, mismatched ${state.engine.mismatched.length}, pack ${state.packs[0]?.report.valid}/${state.packs[0]?.report.total} valid` : 'no report'); check(`${name}: strings changed on screen`, changedTo.length >= 8, `${changedTo.length} chrome strings now in the test dialect, e.g. ${[...new Set(changedTo)].slice(0, 9).map(item => item.slice(2)).join(' | ')}`); // Flash: was any original that the pack replaces ever shown during this load? // Only originals that are gone from the page: a text that another, unreplaced key still shows is no flash. const originals = [...new Set(changedFrom.filter(item => item.startsWith('t:') && !withPack[name].includes(item)).map(item => item.slice(2)))]; const flash = await evaluate(`(() => { const seen = window.__wbSeen ?? new Map(); const originals = ${JSON.stringify(originals)}; const values = ${JSON.stringify([...packValues])}; const first = list => list.map(v => seen.get(v)).filter(v => v !== undefined).sort((a, b) => a - b)[0] ?? null; return { flashed: originals.filter(v => seen.has(v)).map(v => v + '@' + seen.get(v)), firstPack: first(values), texts: seen.size }; })()`); check(`${name}: no flash of X's original text`, flash.flashed.length === 0, `${originals.length} replaced originals watched from document start, ${flash.flashed.length} ever shown${flash.flashed.length ? `: ${flash.flashed.slice(0, 6).join(', ')}` : ''}; first pack text at ${flash.firstPack} ms; ${flash.texts} texts recorded`); summary.pages[name] = { chrome: baseline[name].length, changed: changedTo.length, originalsWatched: originals.length, flashed: flash.flashed.length, firstPackTextMs: flash.firstPack, report: state }; // A second application: X's language module runs again and registers every value once more. const again = await evaluate(`(async () => { let require; window.webpackChunk_twitter_responsive_web.push([[Symbol('wb-check')], {}, r => { require = r; }]); const registry = Object.values(require.c).map(m => m.exports).find(e => e && typeof e === 'object' && typeof e._register === 'function'); const main = [...document.scripts].map(s => s.src).find(s => /\\/main\\./.test(s)); const text = await (await fetch(main)).text(); const lang = document.documentElement.lang; const moduleId = new RegExp('"\\\\./' + lang + '":\\\\[(\\\\d+),').exec(text)?.[1]; if (!registry || !moduleId || !require.c[moduleId]) return { error: 'registry or language module not found', moduleId }; const ids = Object.getOwnPropertyNames(registry).filter(id => /^[a-z][0-9a-f]{7}$/.test(id)); // X's module makes new function objects every time it runs, so functions are compared by what // they return for the same parameters, strings by value. const params = { count: 2, formattedCount: '2', date: 'May 2020', joinDate: 'May 2020', country: 'Nowhere', name: 'n', screenName: 's', fullName: 'f' }; const packIds = new Set(${JSON.stringify(Object.keys(pack.strings))}); const snapshot = () => ids.map(id => { try { const value = registry[id]; return typeof value === 'string' ? value : packIds.has(id) && typeof value === 'function' ? 'fn:' + value(params) : typeof value; } catch { return undefined; } }); const first = snapshot(); const applied = window.wayBetter.stringPacks.report()[0].engine.applied; delete require.c[moduleId]; require(moduleId); const second = snapshot(); let changed = 0; for (let i = 0; i < ids.length; i += 1) if (first[i] !== second[i]) changed += 1; const sample = ['ha8209bc', 'fa98627a'].map(id => registry[id]); return { ids: ids.length, changed, applied, appliedAfter: window.wayBetter.stringPacks.report()[0].engine.applied, sample, message: typeof registry.e2ee95a1 === 'function' ? registry.e2ee95a1({ date: 'May 2020' }) : null, plural: typeof registry.i73fff1b === 'function' ? [registry.i73fff1b({ count: 1 }), registry.i73fff1b({ count: 35 })] : null }; })()`); await wait(1500); const afterSecond = await evaluate(CHROME); const second = differ(withPack[name], afterSecond); check(`${name}: a second application changes nothing further`, !again.error && again.changed === 0 && again.applied === again.appliedAfter && second.onlyA.length + second.onlyB.length === 0, again.error ?? `${again.ids} registry values compared (strings by value, pack messages by their output): ${again.changed} changed; applied ${again.applied} -> ${again.appliedAfter}; chrome strings differing ${second.onlyA.length + second.onlyB.length}; samples ${again.sample.join(', ')} | ${again.message} | ${again.plural?.join(', ')}`); summary.pages[name].second = again; if (name === 'home') { // The emoji names arrive later in their own file, through the same setter. let files = {}; for (let i = 0; i < 24 && !files.emoji; i += 1) { await wait(500); files = (await report())?.[0]?.seen ?? {}; } const state = (await report())?.[0]; check('home: the emoji file went through the same hook', !!files.emoji, `files ${JSON.stringify(files)}, keys seen ${state?.engine.seen}, unlisted ${state?.engine.unlisted.length}`); summary.pages[name].emoji = { files, seen: state?.engine.seen, unlisted: state?.engine.unlisted.length }; } } // The pack again after a reload: the same result. { check('profile: reloaded (pack stored)', await open(`/${handle}`)); const reloaded = differ(numbersAside(withPack.profile), numbersAside(await evaluate(CHROME))); check('profile: a reload gives the same result', reloaded.onlyA.length + reloaded.onlyB.length === 0, `${reloaded.onlyA.length + reloaded.onlyB.length} chrome strings differ`); // Public profile page only; the navigation labels and the profile column, never the account button. // The row naming accounts the signed-in account follows is personal: kept out of the picture. await evaluate(`for (const el of document.querySelectorAll('[data-testid="primaryColumn"] a[href$="/followers_you_follow"]')) el.style.visibility = 'hidden';`); const clip = await evaluate(`(() => { const nav = document.querySelector('header[role="banner"] nav').getBoundingClientRect(); const column = document.querySelector('[data-testid="primaryColumn"]').getBoundingClientRect(); const account = document.querySelector('[data-testid="SideNav_AccountSwitcher_Button"]')?.getBoundingClientRect(); const bottom = Math.min(760, (account?.top ?? 760) - 12); return { x: Math.floor(nav.left), y: 0, width: Math.ceil(column.right - nav.left), height: Math.floor(bottom), scale: 1 }; })()`); const shot = await cdp('Page.captureScreenshot', { format: 'png', clip }).catch(error => ({ error: String(error) })); if (shot.data) { writeFileSync(join(out, 'profile-with-pack.png'), Buffer.from(shot.data, 'base64')); log(`shot ${join(out, 'profile-with-pack.png').replace(/\\/g, '/').split('/').slice(-3).join('/')}`); } else log(`shot failed: ${shot.error}`); } // Cost, measured in this page: the chunk-array wrapper per push and the engine per registered key. { const esbuild = createRequire(join(root, 'package.json'))('esbuild'); const bench = await esbuild.build({ bundle: true, write: false, format: 'iife', minify: true, absWorkingDir: root, stdin: { resolveDir: root, loader: 'ts', contents: ` import { watchChunkArray } from '../../waybetter/packages/runtime/src/stringPackHook.ts'; import { compilePack, createStringEngine, inventoryLookup } from '../../waybetter/packages/runtime/src/stringPacks.ts'; import { languageFile } from './src/inject/stringPacks.ts'; import X_WEB_INVENTORY from './strings/x.web.page.json'; (globalThis as any).__wbBench = (pack: unknown, slice: any) => { const N = 200000; const median = (f: () => number) => [f(), f(), f(), f(), f(), f(), f()].sort((a, b) => a - b)[0]; const chunks = () => Array.from({ length: N }, (_, i) => [[i], {}]); const plain = median(() => { const list = chunks(); const a: unknown[] = []; const t = performance.now(); for (let i = 0; i < N; i += 1) a.push(list[i]); return performance.now() - t; }); const hooked = median(() => { const list = chunks(); const holder: any = { document }; let hits = 0; watchChunkArray(holder, 'a', (_c, script) => { if (languageFile(script)) hits += 1; }); const a = holder.a; const t = performance.now(); for (let i = 0; i < N; i += 1) a.push(list[i]); return performance.now() - t; }); const t0 = performance.now(); const compiled = compilePack(pack, slice); inventoryLookup(slice)('x'); const setup = performance.now() - t0; const ids = [...X_WEB_INVENTORY.text.split(' '), ...Object.keys(X_WEB_INVENTORY.message), ...X_WEB_INVENTORY.closed.split(' ')]; const fn = () => ''; const values = ids.map(id => (X_WEB_INVENTORY.message[id] !== undefined ? fn : 'x')); const direct = median(() => { const table: Record = {}; const t = performance.now(); for (let i = 0; i < ids.length; i += 1) table[ids[i]!] = values[i]; return performance.now() - t; }); const engine = median(() => { const e = createStringEngine(slice, [compiled]); const table: Record = {}; const t = performance.now(); for (let i = 0; i < ids.length; i += 1) table[ids[i]!] = e.resolve(ids[i]!, values[i]); return performance.now() - t; }); return { N, plainNs: Math.round(plain * 1e6 / N), hookedNs: Math.round(hooked * 1e6 / N), setupMs: +setup.toFixed(2), keys: ids.length, directMs: +direct.toFixed(2), engineMs: +engine.toFixed(2) }; };` }, }); const cost = await evaluate(`${bench.outputFiles[0].text}; __wbBench(${JSON.stringify(pack)}, ${JSON.stringify(delivery.inventory)})`); const objectAssign = await evaluate('Function.prototype.toString.call(Object.assign).includes("[native code]")'); log(`cost chunk push: ${cost.plainNs} ns plain, ${cost.hookedNs} ns through the wrapper (${cost.N} pushes, best of 7); engine: ${cost.keys} keys in ${cost.engineMs} ms vs ${cost.directMs} ms direct; pack compile against its inventory slice ${cost.setupMs} ms; Object.assign is native (untouched): ${objectAssign}`); summary.cost = { ...cost, objectAssignNative: objectAssign }; } // 3. Without the pack again: X's own text, exactly. await storePack(false); for (const [name, path] of pagesOf) { check(`${name}: page loaded (pack removed)`, await open(path)); const restored = await evaluate(CHROME); const exact = differ(baseline[name], restored); const wording = differ(numbersAside(baseline[name]), numbersAside(restored)); const state = await evaluate(`({ report: window.wayBetter?.stringPacks?.report?.() ?? null, pushIsPlain: !Object.getOwnPropertyDescriptor(window.webpackChunk_twitter_responsive_web ?? [], 'push')?.get, dialect: document.body.innerText.includes('Hoom') })`); check(`${name}: X's text is back exactly`, wording.onlyA.length + wording.onlyB.length === 0 && state.report.length === 0 && state.pushIsPlain && !state.dialect, `${restored.length} chrome strings; wording differences ${wording.onlyA.length + wording.onlyB.length}; with live numbers ${exact.onlyA.length + exact.onlyB.length}${wording.onlyA.length + wording.onlyB.length ? `: ${[...wording.onlyA, ...wording.onlyB].slice(0, 6).map(item => item.length > 40 ? item.length + ' chars' : item).join(' | ')}` : ''}; nothing hooked`); summary.pages[name].restored = { wordingDifferences: wording.onlyA.length + wording.onlyB.length, withNumbers: exact.onlyA.length + exact.onlyB.length }; } } finally { await storePack(false).catch(() => {}); const left = await evaluate(`sessionStorage.getItem(${JSON.stringify(KEY)})`).catch(() => 'unknown'); log(`cleanup pack in the tab's session storage: ${left === null ? 'removed' : left}`); log(`pages before closing the check's tab: ${await pages().catch(() => '?')} (${pagesBefore} before the check)`); ws.close(); await fetch(`${endpoint}/json/close/${own.id}`).catch(() => {}); await wait(1500); log(`pages after: ${await pages().catch(() => '?')}; app processes alive: ${alive()}`); } const failed = lines.filter(line => line.startsWith('FAIL')).length; log(`${lines.filter(line => line.startsWith('PASS')).length} passed, ${failed} failed`); writeFileSync(join(out, 'live-check.json'), `${JSON.stringify(summary, null, 1)}\n`); writeFileSync(join(out, 'live-check.txt'), `${lines.join('\n')}\n`); process.exitCode = failed ? 1 : 0;