v6.7.3: faster loading, and pages search engines can read

The bundles carried all thirteen translations at once and a page fetched them a
third time as raw source; each page now loads only its own language. Alongside
that: JavaScript is minified, the eight stylesheets are served as one file, the
QR scanner is fetched after the app is up instead of on every visit, Inter ships
once rather than five copies of the same file, and Font Awesome is subset to the
82 icons this app draws instead of all 2468.

1.85 MB across 43 requests becomes under 700 KB across 33. On mobile the page
starts drawing in 1.6 s instead of 6.3 s and is usable in 4.5 s instead of 11 s.

Pages also carry their text in the HTML now. Everything was drawn by JavaScript
into an empty div, so crawlers saw correct metadata around nothing, and twelve of
the thirteen language pages had never been shown to anyone. The documentation is
published under /docs/ with a new FAQ, and unknown addresses return a real 404.

Separately: the localized shells were served with the year-long immutable cache
header meant for static assets, which pinned anyone who opened /de/ or /ru/ to
that build. The header is fixed and the service worker refreshes what it cached.

Claude-Session: https://claude.ai/code/session_014KjzTXxrhzYoDDWChYQ4u2
This commit is contained in:
lockbitchat
2026-09-04 00:41:46 -04:00
parent 0691ce618c
commit 414c27fda6
111 changed files with 19469 additions and 107393 deletions
+10 -4
View File
@@ -255,10 +255,16 @@ assert.ok(!app.includes("behavior: 'smooth'"),
"no raw behavior:'smooth' — it must go through scrollBehavior()");
assert.match(app, /const scrollBehavior = \(\) =>/);
// The stylesheet has to load, and it has to load last.
assert.match(html, /apple-motion\.css/, 'apple-motion.css must be linked');
const idxMotion = html.indexOf('apple-motion.css');
const idxComponents = html.indexOf('components.css');
// The stylesheet has to reach the page, and it has to come after the one it argues
// with. The sheets are no longer linked individually — scripts/build-css.js concatenates
// them into assets/app.css to save eight render-blocking round trips — so the order that
// matters is the order in that list, which is the order they end up in the file.
assert.match(html, /\/assets\/app\.css/, 'the page must link the bundled stylesheet');
const cssOrder = [...readFileSync(new URL('../scripts/build-css.js', import.meta.url), 'utf8')
.matchAll(/'((?:src|assets)\/[^']+\.css)'/g)].map((m) => m[1]);
const idxMotion = cssOrder.indexOf('src/styles/apple-motion.css');
const idxComponents = cssOrder.indexOf('src/styles/components.css');
assert.ok(idxMotion !== -1, 'apple-motion.css is not in the CSS bundle, so it never reaches the page');
assert.ok(idxMotion > idxComponents,
'apple-motion.css must come after components.css — it settles arguments on source order');
+113
View File
@@ -0,0 +1,113 @@
// The documentation is the only text on this site written to answer a question someone
// actually types. It spent its life as raw Markdown behind Disallow: /doc/, which is a
// large part of why every category query lands on page three. What has to keep holding
// once it is published: one page per document, each one addressable, describable and
// reachable — a page Google cannot find a link to, or cannot tell apart from another,
// is back where it started.
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { readdirSync, readFileSync, existsSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = fileURLToPath(new URL('..', import.meta.url));
const read = (rel) => readFileSync(path.join(ROOT, rel), 'utf8');
// Regenerate first, so the committed pages are checked as *output* and a stale docs/
// fails here rather than shipping.
execFileSync('node', [path.join(ROOT, 'scripts/build-docs.js')], { stdio: 'pipe' });
const sources = readdirSync(path.join(ROOT, 'doc')).filter((f) => f.endsWith('.md'));
assert.ok(sources.length >= 2, 'doc/ should hold the documentation this test is about');
const urlFor = (file) =>
file === 'README.md' ? '/docs/' : `/docs/${path.basename(file, '.md').toLowerCase()}/`;
const fileFor = (url) => path.join('docs', url.slice('/docs/'.length), 'index.html');
const titles = new Set();
const descriptions = new Set();
for (const source of sources) {
const url = urlFor(source);
const rel = fileFor(url);
assert.ok(existsSync(path.join(ROOT, rel)), `${source} has no page at ${url}`);
const html = read(rel);
// Every page must be addressable as itself. A shared canonical would collapse all
// nine into one result, which is the failure this whole exercise is undoing.
assert.ok(html.includes(`<link rel="canonical" href="https://securebit.chat${url}">`),
`${rel}: canonical must point at ${url}`);
const title = html.match(/<title>([\s\S]*?)<\/title>/);
assert.ok(title, `${rel}: no <title>`);
assert.ok(!titles.has(title[1]), `${rel}: duplicate <title> ${title[1]}`);
titles.add(title[1]);
const description = html.match(/<meta name="description" content="([^"]*)">/);
assert.ok(description && description[1].length > 40, `${rel}: description is missing or too thin`);
assert.ok(description[1].length <= 170,
`${rel}: description is ${description[1].length} chars — Google truncates near 155`);
assert.ok(!descriptions.has(description[1]), `${rel}: duplicate description`);
descriptions.add(description[1]);
assert.ok(html.includes('<h1'), `${rel}: no <h1>`);
// Links carried over from Markdown must have been rewritten. A surviving .md href
// is a 404 for a reader and a dead end for a crawler.
const dangling = [...html.matchAll(/href="([^"]*\.md[^"]*)"/g)]
.map((m) => m[1])
.filter((href) => !href.startsWith('https://github.com/'));
assert.deepEqual(dangling, [], `${rel}: unrewritten Markdown links`);
// Nothing may reference an asset relatively: /docs/cryptography/ + "logo/x.png"
// resolves under the document's own directory and 404s.
const relative = [...html.matchAll(/\b(?:src|href)="(?!https?:|data:|mailto:|#|\/)([^"]+)"/g)];
assert.deepEqual(relative.map((m) => m[1]), [], `${rel}: relative asset path`);
assert.ok(html.includes('href="/docs/'), `${rel}: no link back into the documentation`);
assert.ok(html.includes('href="/"'), `${rel}: no link back to the app`);
}
// The FAQ is the one document that is a list of questions, and the only one that may
// say so. An earlier version detected FAQ shape from "two or more <h2>", which every
// document here satisfies, and published ARCHITECTURE.md as a FAQPage — structured data
// describing a page as something it is not is worse than shipping none.
{
const faq = read(fileFor('/docs/faq/'));
const schemaOf = (html) => JSON.parse(html.match(/application\/ld\+json">([\s\S]*?)<\/script>/)[1]);
const faqSchema = schemaOf(faq);
assert.equal(faqSchema['@type'], 'FAQPage', 'the FAQ must be marked up as one');
assert.ok(faqSchema.mainEntity.length >= 5, 'the FAQ schema lost its questions');
for (const entry of faqSchema.mainEntity) {
assert.equal(entry['@type'], 'Question');
assert.ok(entry.name.length > 5 && entry.acceptedAnswer.text.length > 40,
`FAQ entry "${entry.name}" is missing its question or answer`);
}
// Every question in the schema must be a heading a reader can actually see.
for (const entry of faqSchema.mainEntity) {
assert.ok(faq.includes(entry.name.replace(/&/g, '&amp;')),
`FAQ schema claims a question the page does not show: ${entry.name}`);
}
for (const source of sources.filter((f) => f !== 'FAQ.md')) {
assert.equal(schemaOf(read(fileFor(urlFor(source))))['@type'], 'TechArticle',
`${source} must not be published as a FAQPage`);
}
}
// Discoverability: the sitemap lists them, and the landing page links to them. The
// sitemap alone only says the pages exist; the link is what gets them crawled.
const sitemap = read('sitemap.xml');
const landing = read('index.html');
for (const source of sources) {
const url = urlFor(source);
assert.ok(sitemap.includes(`<loc>https://securebit.chat${url}</loc>`), `sitemap is missing ${url}`);
assert.ok(landing.includes(`href="${url}"`), `the landing page does not link to ${url}`);
}
assert.ok(/<lastmod>\d{4}-\d{2}-\d{2}<\/lastmod>/.test(sitemap), 'sitemap entries need a lastmod');
// The raw Markdown must stay out of the index: same text, second address.
assert.ok(read('robots.txt').includes('Disallow: /doc/'),
'the raw doc/*.md files must stay disallowed now that /docs/ carries the same text');
console.log(`docs-build.test.mjs: ${sources.length} documentation pages checked`);
+90 -10
View File
@@ -7,7 +7,7 @@
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { readFileSync, writeFileSync, existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
import { readdirSync, readFileSync, writeFileSync, existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
@@ -94,9 +94,13 @@ for (const code of site.locales) {
const loc = `<loc>${urlFor(code)}</loc>`;
assert.equal(sitemap.split(loc).length - 1, 1, `sitemap.xml must list ${urlFor(code)} exactly once`);
}
// The sitemap also carries the documentation pages, which are not locales and have
// no hreflang cluster of their own. Count them from doc/ rather than hard-coding a
// number, so adding a document does not fail this for the wrong reason.
const docCount = readdirSync(path.join(ROOT, 'doc')).filter((f) => f.endsWith('.md')).length;
assert.equal(
(sitemap.match(/<url>/g) || []).length, site.locales.length,
'sitemap.xml has entries for URLs that are not locales'
(sitemap.match(/<url>/g) || []).length, site.locales.length + docCount,
'sitemap.xml has entries that are neither a locale nor a documentation page'
);
}
@@ -240,6 +244,33 @@ for (const code of site.locales) {
}
assert.ok(secondary.includes('<meta property="og:locale:alternate" content="en_US">'));
// Every page must carry its own text inside <div id="root">, not just metadata
// around an empty div. This is the whole reason the locales have separate URLs:
// pages that differ only in <meta> gave Google nothing to prefer, and eleven of
// the thirteen were never shown to anyone. React empties the container on mount,
// so the only contract to keep is that the strings come from the locale file.
for (const [page, name] of [[primary, 'en'], [secondary, 'xx']]) {
const shell = page.slice(page.indexOf('<div id="root">'), page.indexOf('</body>'));
const heading = shell.match(/<h1>([\s\S]*?)<\/h1>/);
assert.ok(heading, `${name}: the prerendered landing must carry an <h1>`);
assert.ok(heading[1].includes(en.ui['hero.headlineTop']),
`${name}: the <h1> must be the locale's own headline, not a placeholder`);
assert.ok(shell.includes(en.ui['unique.heading']), `${name}: lost the feature section`);
assert.ok(shell.includes(en.ui['roadmap.heading']), `${name}: lost the roadmap section`);
assert.ok(!/\{\{|undefined|\[object /.test(shell),
`${name}: the prerendered landing leaked an unresolved value`);
// The mark that stands in until React mounts. Without something contentful in
// the markup the first paint is the app itself, which measured 6.3 s on mobile.
// It must be visible by default — the text block is the one hidden behind
// <noscript> — and it must be inline, or it costs a request to be a placeholder.
assert.ok(shell.includes('class="sb-boot"'), `${name}: no loading mark before the app mounts`);
assert.match(shell, /<div class="sb-boot"[^>]*>\s*<svg/,
`${name}: the loading mark must be inline SVG, not a fetched image`);
assert.ok(shell.includes('.sb-boot{display:none}'),
`${name}: the loading mark must give way to the text block when there is no JavaScript`);
}
// Nothing on a subdirectory page may use a relative asset path: "src/app.js" from
// /xx/ resolves to /xx/src/app.js and 404s. CSP forbids <base>, so absolute is the
// only option available.
@@ -266,16 +297,65 @@ for (const code of site.locales) {
'scripts/build-i18n.js must stamp the secondary locales into sw.js');
assert.ok(sw.includes('LOCALE_SHELLS'), 'sw.js lost the localized-shell wiring');
// Both served configs must route deep links inside a locale to that locale's shell.
const nginx = at('deploy/nginx.conf');
assert.ok(nginx.includes('~^/xx/ /xx/index.html;'), 'nginx.conf lost its locale shell entry');
assert.ok(nginx.includes('try_files $uri $uri/ $sb_shell;'), 'nginx.conf must use the shell map');
const htaccess = at('.htaccess');
assert.ok(htaccess.includes('RewriteRule ^xx(/.*)?$ /xx/index.html [L]'), '.htaccess lost its locale rewrite');
// Neither served config may answer an unknown address with the app shell. Doing so
// returns 200 OK for every typo and every scanner probe, which is how an infinite
// URL space becomes indexable pages and Search Console fills with soft 404s. These
// read the repo's own configs, not the throwaway render: the locale list is no
// longer stamped into them, because try_files and mod_dir resolve /xx/ on their own.
const nginx = readFileSync(path.join(ROOT, 'deploy/nginx.conf'), 'utf8');
assert.ok(nginx.includes('try_files $uri $uri/ =404;'),
'nginx.conf must refuse unknown paths, not fall back to the app shell');
assert.ok(nginx.includes('error_page 404 /404.html;'), 'nginx.conf lost its 404 page');
// Every app shell must land in the no-cache group. The patterns here were once
// anchored at the root only (~^/index\.html$, ~^/$), so /de/ and /ru/index.html
// fell through to the one-year immutable default and a visitor who opened a
// localized page was frozen on that build — new releases could not reach them.
// Read the map rather than the source line, so the assertion is about behaviour.
{
const block = nginx.match(/map \$uri \$sb_cache \{([\s\S]*?)\n \}/);
assert.ok(block, 'nginx.conf lost the $sb_cache map');
// A map key is either bare or double-quoted; a regex containing { or } has to
// be quoted, because nginx otherwise reads the brace as the start of a block
// and refuses to boot. That failure is invisible until deploy, so pin it here.
const entries = [...block[1].matchAll(/^\s*(?:"([^"]+)"|(\S+))\s+"([^"]*)";/gm)]
.map((m) => ({ key: m[1] ?? m[2], quoted: m[1] !== undefined, value: m[3] }));
for (const entry of entries) {
if (/[{}]/.test(entry.key)) {
assert.ok(entry.quoted,
`nginx.conf: ${entry.key} contains a brace and must be quoted, or nginx will not start`);
}
}
const noCache = entries
.filter((e) => e.key.startsWith('~') && e.value.startsWith('no-cache'))
.map((e) => new RegExp(e.key.slice(1)));
const shells = ['/', '/index.html', ...site.locales
.filter((c) => c !== site.defaultLocale)
.flatMap((c) => [`/${c}/`, `/${c}/index.html`, `/${c}/manifest.json`])];
for (const shell of [...shells, '/manifest.json', '/sw.js', '/meta.json']) {
assert.ok(noCache.some((re) => re.test(shell)),
`nginx.conf would cache ${shell} for a year — it must revalidate`);
}
// The point of the immutable default is still that static assets keep it.
// /dist/ is excluded on purpose: those revalidate by design, so that a release
// is picked up immediately while an unchanged bundle still comes back as a 304.
for (const asset of ['/assets/tailwind.css', '/logo/icon-192x192.png', '/assets/fonts/inter/inter.css']) {
assert.equal(noCache.some((re) => re.test(asset)), false,
`nginx.conf now refuses to cache ${asset}, which should stay immutable`);
}
}
const htaccess = readFileSync(path.join(ROOT, '.htaccess'), 'utf8');
assert.ok(!/RewriteRule \^\(\.\*\)\$ \/index\.html/.test(htaccess),
'.htaccess must not rewrite unknown paths to the app shell');
assert.ok(htaccess.includes('ErrorDocument 404 /404.html'), '.htaccess lost its 404 page');
assert.ok(existsSync(path.join(ROOT, '404.html')), 'both configs point at a 404.html that must exist');
// The sitemap must carry the alternates too, not just the URLs.
const sitemap = at('sitemap.xml');
assert.equal((sitemap.match(/<url>/g) || []).length, 2);
// Two locales plus the documentation, which is rendered from the real doc/ and is
// not part of the throwaway locale fixture.
const docCount = readdirSync(path.join(ROOT, 'doc')).filter((f) => f.endsWith('.md')).length;
assert.equal((sitemap.match(/<url>/g) || []).length, 2 + docCount);
assert.ok(sitemap.includes('<xhtml:link rel="alternate" hreflang="xx" href="https://securebit.chat/xx/"/>'));
rmSync(tmp, { recursive: true, force: true });
+82 -18
View File
@@ -23,17 +23,23 @@ const ROOT = fileURLToPath(new URL('..', import.meta.url));
);
}
// The dictionaries have to reach the bundle the browser actually downloads, not just
// the module on disk. `build:js` bundles whatever src/i18n/generated.js says at the
// moment it runs, so if generation happened after bundling, a newly added language
// would ship a correct page — right <html lang>, right <html dir> — wrapped around an
// app that has never heard of it and quietly falls back to English. That failure looks
// like a translation bug and is really a build-order bug, so it is pinned here.
// A dictionary has to reach the page that needs it — and only that page.
//
// Every locale used to be bundled into dist/app.js, into dist/app-boot.js, and fetched
// a third time as raw source by the page modules that import the runtime directly: 215 KB
// transferred to hand a Russian reader twelve dictionaries they will never read. Now the
// default locale is bundled (t() falls back to it) and each other locale is a module its
// own shell loads. Both halves of that are pinned here, because either one failing is
// silent: bundling them all again only shows up as a slow page, and dropping the shell's
// script only shows up as an English page under a Russian <html lang>.
{
const bundle = readFileSync(path.join(ROOT, 'dist/app.js'), 'utf8');
const { SUPPORTED_LOCALES, DICTIONARIES } = await import(
const { DEFAULT_LOCALE, SUPPORTED_LOCALES } = await import(
pathToFileURL(path.join(ROOT, 'src/i18n/generated.js'))
);
const dictOf = async (code) => (await import(
pathToFileURL(path.join(ROOT, `src/i18n/dict/${code}.js`))
)).DICTIONARY;
// esbuild escapes anything outside ASCII, so nothing but pure Latin is findable as
// literal text: Latin-1 becomes \xNN, everything above it \uXXXX, both uppercase.
@@ -49,14 +55,44 @@ const ROOT = fileURLToPath(new URL('..', import.meta.url));
})
.join('');
const absent = SUPPORTED_LOCALES.filter(
(code) => !bundle.includes(asEmitted(DICTIONARIES[code]['hero.headlineTop']))
);
assert.deepEqual(absent, [],
`dist/app.js carries no strings for: ${absent.join(', ')} — run \`npm run build\`, ` +
const headlineOf = async (code) => (await dictOf(code))['hero.headlineTop'];
assert.ok(bundle.includes(asEmitted(await headlineOf(DEFAULT_LOCALE))),
`dist/app.js carries no strings for the default locale — run \`npm run build\`, ` +
'and check that build:i18n still runs before build:js');
assert.ok(bundle.includes(`SUPPORTED_LOCALES = ${JSON.stringify(SUPPORTED_LOCALES).replace(/,/g, ', ')}`),
// The saving is the assertion. If another locale turns up in the bundle, something
// has started importing dictionaries statically again and the page grew by 200 KB.
for (const code of SUPPORTED_LOCALES.filter((c) => c !== DEFAULT_LOCALE)) {
assert.equal(bundle.includes(asEmitted(await headlineOf(code))), false,
`dist/app.js bundles the ${code} dictionary — only the default locale belongs in it`);
}
// ...and each of those locales must still reach its own page.
const site = JSON.parse(readFileSync(path.join(ROOT, 'locales/site.json'), 'utf8'));
for (const code of SUPPORTED_LOCALES) {
const shell = readFileSync(
path.join(ROOT, code === site.defaultLocale ? 'index.html' : `${code}/index.html`), 'utf8'
);
const tag = `src="/src/i18n/dict/${code}.js`;
if (code === site.defaultLocale) {
assert.equal(shell.includes('/src/i18n/dict/'), false,
'the default locale already has its dictionary in the bundle; loading it again is dead weight');
} else {
assert.ok(shell.includes(tag), `${code}/index.html never loads its own dictionary`);
// It has to run before anything that asks for a string.
assert.ok(shell.indexOf(tag) < shell.indexOf('/dist/app-boot.js'),
`${code}/index.html loads its dictionary after the app that reads it`);
}
}
// The registry has to survive into the bundle as a whole list, not just as loose
// strings the assertion above would also accept. What it must NOT depend on is the
// shape esbuild emits: --minify drops the spaces after the commas and renames the
// binding, so match the array literal in either spelling and never the variable
// name — otherwise turning minification on reads as a missing locale.
const registry = JSON.stringify(SUPPORTED_LOCALES);
assert.ok(bundle.includes(registry) || bundle.includes(registry.replace(/,/g, ', ')),
'the bundled locale registry disagrees with src/i18n/generated.js');
}
@@ -80,6 +116,15 @@ const ROOT = fileURLToPath(new URL('..', import.meta.url));
const tmp = mkdtempSync(path.join(tmpdir(), 'sb-i18n-win-'));
copyFileSync(path.join(ROOT, 'src/i18n/generated.js'), path.join(tmp, 'generated.js'));
copyFileSync(path.join(ROOT, 'src/i18n/index.js'), path.join(tmp, 'index.js'));
// index.js imports its default dictionary for the fallback, so a copy of the
// module is only a working copy if that comes with it.
const { DEFAULT_LOCALE: fallbackLocale } = await import(
pathToFileURL(path.join(ROOT, 'src/i18n/generated.js'))
);
mkdirSync(path.join(tmp, 'dict'), { recursive: true });
for (const name of ['default.js', `${fallbackLocale}.js`]) {
copyFileSync(path.join(ROOT, 'src/i18n/dict', name), path.join(tmp, 'dict', name));
}
const partial = await import(pathToFileURL(path.join(tmp, 'index.js')));
assert.equal(partial.currentLocale(), partial.DEFAULT_LOCALE,
'a window without location must fall back to the default locale, not throw');
@@ -95,7 +140,7 @@ const ROOT = fileURLToPath(new URL('..', import.meta.url));
// copy of the module in a temp directory and exercise it there.
{
const tmp = mkdtempSync(path.join(tmpdir(), 'sb-i18n-rt-'));
mkdirSync(tmp, { recursive: true });
mkdirSync(path.join(tmp, 'dict'), { recursive: true });
writeFileSync(path.join(tmp, 'generated.js'), `
export const DEFAULT_LOCALE = "en";
export const SUPPORTED_LOCALES = ["en", "de"];
@@ -103,13 +148,26 @@ export const LOCALE_META = {
en: { htmlLang: "en", nativeName: "English", dir: "ltr", path: "/" },
de: { htmlLang: "de", nativeName: "Deutsch", dir: "ltr", path: "/de/" }
};
export const DICTIONARIES = {
en: { "greeting": "Hello", "only.en": "English only", "welcome": "Hello, {name}" },
de: { "greeting": "Hallo" }
};
export const CROSS_LOCALE_STRINGS = { en: {}, de: { "language.suggest.cta": "Auf Deutsch lesen" } };
`);
// Dictionaries register themselves on a global, the same way the generated ones do.
const fixtureDict = (code, entries) => writeFileSync(path.join(tmp, 'dict', `${code}.js`), `
export const DICTIONARY = ${JSON.stringify(entries)};
const registry = globalThis.__SECUREBIT_I18N__ || (globalThis.__SECUREBIT_I18N__ = Object.create(null));
registry[${JSON.stringify(code)}] = DICTIONARY;
`);
fixtureDict('en', { greeting: 'Hello', 'only.en': 'English only', welcome: 'Hello, {name}' });
fixtureDict('de', { greeting: 'Hallo' });
writeFileSync(path.join(tmp, 'dict', 'default.js'), 'import "./en.js";\n');
copyFileSync(path.join(ROOT, 'src/i18n/index.js'), path.join(tmp, 'index.js'));
// That global is shared with the real module imported earlier in this file, so the
// fixture is swapped in around this block rather than left to overwrite it.
const savedRegistry = globalThis.__SECUREBIT_I18N__;
globalThis.__SECUREBIT_I18N__ = Object.create(null);
const i18n = await import(pathToFileURL(path.join(tmp, 'index.js')));
// The German dictionary is one the fixture's page would have loaded for itself.
await import(pathToFileURL(path.join(tmp, 'dict', 'de.js')));
// Reading a path.
assert.equal(i18n.localeFromPathname('/de/'), 'de');
@@ -173,6 +231,12 @@ export const DICTIONARIES = {
assert.equal(i18n.t('welcome', { name: 'Ada' }, 'en'), 'Hello, Ada');
assert.equal(i18n.t('welcome', {}, 'en'), 'Hello, {name}', 'an absent variable stays literal rather than blank');
// A locale whose dictionary this page never loaded still answers for the handful of
// keys the language suggestion needs, and falls back to the default for the rest.
assert.equal(i18n.t('language.suggest.cta', null, 'de'), 'Auf Deutsch lesen',
'the suggestion bar must speak the language it is offering');
globalThis.__SECUREBIT_I18N__ = savedRegistry;
rmSync(tmp, { recursive: true, force: true });
}
+64
View File
@@ -0,0 +1,64 @@
// The icon fonts are subset to what this interface draws: 82 of Font Awesome's 2468,
// which took 299 KB of woff2 down to 12 KB. The saving comes with a failure mode that
// is completely silent — add an icon to a component and it renders as an empty box,
// with no error anywhere, because the glyph is simply not in the shipped font.
//
// So the manifest scripts/subset-icons.py writes is checked against the source here.
// A new icon fails the build with the name of the icon and the command to run.
import assert from 'node:assert/strict';
import { readdirSync, readFileSync, existsSync, statSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = fileURLToPath(new URL('..', import.meta.url));
const read = (rel) => readFileSync(path.join(ROOT, rel), 'utf8');
const manifest = JSON.parse(read('assets/fontawesome/subset-icons.json'));
const covered = new Set(manifest.icons);
const notIcons = new Set(manifest.notIcons);
// Every fa- class the interface names, from the same files the subsetter reads.
function sourceFiles(dir) {
const out = [];
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) out.push(...sourceFiles(full));
else if (/\.(js|jsx|css|html)$/.test(entry.name)) out.push(full);
}
return out;
}
const used = new Set();
for (const file of [...sourceFiles(path.join(ROOT, 'src')), path.join(ROOT, 'index.html')]) {
for (const match of readFileSync(file, 'utf8').matchAll(/\bfa-([a-z0-9-]+)/g)) {
if (!notIcons.has(match[1])) used.add(match[1]);
}
}
const missing = [...used].filter((name) => !covered.has(name)).sort();
assert.deepEqual(missing, [],
`these icons are used but not in the subset, so they render as empty boxes: ${missing.join(', ')}\n` +
' fix: pip install fonttools brotli && python3 scripts/subset-icons.py && npm run build');
// The subset fonts and the stylesheet that names them have to exist and be small — the
// point of the exercise is the size, and a regenerate that quietly fell back to the full
// font would still pass every assertion above.
const bundle = read('assets/app.css');
for (const family of ['fa-solid-900', 'fa-regular-400', 'fa-brands-400']) {
const rel = `assets/fontawesome/webfonts/${family}.subset.woff2`;
assert.ok(existsSync(path.join(ROOT, rel)), `${rel} is missing — run scripts/subset-icons.py`);
const size = statSync(path.join(ROOT, rel)).size;
assert.ok(size < 40_000, `${rel} is ${size} B — that is the full font, not a subset`);
assert.ok(bundle.includes(`${family}.subset.woff2`), `assets/app.css does not use the ${family} subset`);
}
// And the full stylesheet must not come back: it is 102 KB for icons that are not here.
assert.equal(bundle.includes('/assets/fontawesome/css/all.min.css'), false,
'the full Font Awesome stylesheet is being loaded again alongside the subset');
for (const shell of ['index.html', 'ru/index.html']) {
assert.equal(read(shell).includes('fontawesome/css/all.min.css'), false,
`${shell} still links the full Font Awesome stylesheet`);
}
console.log(`icon-subset.test.mjs: ${used.size} icons used, all covered by the subset`);
+58
View File
@@ -0,0 +1,58 @@
// The QR bundle is the third largest thing this site serves — 142 KB gzipped — and it
// is needed on exactly two occasions: creating a channel, and opening the scanner.
// Neither is on the first screen. It used to be a <script type="module"> in <head>, so
// every first visit paid for it up front. What has to keep holding: it is not in the
// shells, app-boot still fetches it, esbuild has not quietly inlined it back into the
// critical bundle, and the scanner can still start if someone opens it early.
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = fileURLToPath(new URL('..', import.meta.url));
const read = (rel) => readFileSync(path.join(ROOT, rel), 'utf8');
const site = JSON.parse(read('locales/site.json'));
const shells = ['index.html', ...site.locales.filter((c) => c !== site.defaultLocale).map((c) => `${c}/index.html`)];
for (const shell of shells) {
const html = read(shell);
// Comments are allowed to mention it; a <script src> is not.
const tags = [...html.matchAll(/<script[^>]*src="([^"]*)"/g)].map((m) => m[1]);
assert.equal(tags.some((src) => src.includes('qr-local')), false,
`${shell} loads the QR bundle up front — it belongs behind app-boot's idle fetch`);
assert.equal(tags.some((src) => src.includes('QRScanner')), false,
`${shell} still loads QRScanner.js, which registers a window.QRScanner nothing reads`);
}
// app-boot must actually fetch it, and only after the app has mounted.
{
const boot = read('src/scripts/app-boot.js');
assert.match(boot, /import\(\s*['"]\/dist\/qr-local\.js['"]\s*\)/,
'app-boot.js no longer imports the QR bundle — nothing else does either');
assert.ok(boot.includes('scheduleQrBundle()'), 'app-boot.js never schedules the QR fetch');
assert.ok(boot.includes('securebit:qr-ready'),
'app-boot.js must announce the bundle, or a scanner opened early never starts');
// The import has to stay a runtime fetch. If esbuild ever resolves and inlines it,
// the bundle grows by the whole QR library and this change silently undoes itself.
const bundle = read('dist/app-boot.js');
assert.ok(bundle.includes('import("/dist/qr-local.js")'),
'dist/app-boot.js lost the dynamic import — esbuild may have inlined the QR bundle');
assert.equal(bundle.includes('Html5Qrcode'), false,
'the QR library ended up inside dist/app-boot.js, which is what deferring it was for');
}
// The scanner starts from an effect guarded on window.Html5Qrcode. With the bundle
// arriving late that guard can fail on first run, so the effect has to re-run when it
// lands — otherwise opening the modal early leaves a black viewfinder for good.
{
const app = read('src/app.jsx');
assert.ok(app.includes("window.addEventListener('securebit:qr-ready'"),
'app.jsx does not listen for the QR bundle, so an early scanner never recovers');
assert.match(app, /\}, \[showQRScannerModal, qrBundleReady\]\);/,
'the scanner effect must depend on the QR bundle having arrived');
}
console.log('qr-bundle-deferred.test.mjs: all assertions passed');
+18 -2
View File
@@ -50,8 +50,24 @@ function sources() {
`${code}: an installed PWA would launch left-to-right`);
}
assert.ok(read('templates/index.template.html').includes('src/styles/rtl.css'),
'the mirroring stylesheet is not linked from the template');
// The sheets are no longer linked one by one; scripts/build-css.js concatenates
// them into assets/app.css to save eight render-blocking round trips. So check the
// two things that actually matter: rtl.css is still in the bundle's list, and it is
// still last in it — its rules exist to win over everything above them.
const cssBuild = read('scripts/build-css.js');
const order = [...cssBuild.matchAll(/'((?:src|assets)\/[^']+\.css)'/g)].map((m) => m[1]);
assert.ok(order.includes('src/styles/rtl.css'),
'the mirroring stylesheet is not in the CSS bundle, so it never reaches the page');
assert.ok(order.indexOf('src/styles/rtl.css') > order.indexOf('src/styles/components.css'),
'rtl.css must come after the sheets it overrides, or the mirroring loses on source order');
assert.ok(read('templates/index.template.html').includes('/assets/app.css'),
'the template does not link the bundled stylesheet');
// And that the built file really carries them: a bundler that silently dropped an
// input would leave every one of these assertions above still passing.
const bundled = read('assets/app.css');
assert.match(bundled, /\[dir=["']?rtl["']?\]/,
'assets/app.css carries no right-to-left rules (the minifier drops the quotes)');
}
// ── The runtime exposes direction ────────────────────────────────────────────────────