library/i18n/index.js globbed every catalogue area into every bundle: 25 areas x 11 locales, other plugins' strings included, and each new area grew every bundle (CRITIC-10). - internal/i18n-areas.mjs walks a bundle's SOURCE import graph (relative, @map/, dynamic import, import.meta.glob) and keeps an area when its name appears as a key prefix or a bare string literal anywhere reached; runtime-fetched data carries no i18n keys (measured), and every rule errs toward including. - vite.config.js: in a BUNDLE build, a pre-transform narrows index.js's glob to those areas; dev keeps all. - check:i18n-treeshake (gates:post): every area-prefixed key literal in each BUILT bundle belongs to an included area. Poisoned (one area dropped) it fails. Measured, raw (gzip): people-groups-grid 611,595 -> 210,308 (203,176 -> 72,658); wall 659,876 -> 332,644; doxa-simple-map 902,907 -> 686,910; research-dashboard 843,625 -> 637,940.
37 lines
2.3 KiB
JavaScript
37 lines
2.3 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* check-i18n-treeshake — each built bundle carries every translation area its code asks for, and (with
|
|
* --sizes) reports what the narrowing saved. Headless.
|
|
*
|
|
* CRITIC-10 (2026-09-26): the build narrows library/i18n's catalogue glob to the areas
|
|
* internal/i18n-areas.mjs finds in the bundle's source graph. Omitting an area a bundle uses would show
|
|
* raw keys instead of text, so this re-reads the BUILT output: every "area.key" literal (and every bare
|
|
* 'area' literal, for keys assembled at runtime) in <bundle>.js must belong to an area the build included
|
|
* for that bundle. Only area-PREFIXED literals are judged here (a bare 'template' or 'options' is also
|
|
* ordinary Vue/JS vocabulary); the build includes an area on a bare literal too, which only over-includes.
|
|
* EXIT 0 pass · 1 an area the bundle needs was left out
|
|
*/
|
|
import { readFileSync, existsSync, readdirSync } from 'node:fs'
|
|
import { join, resolve, dirname } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { TREE_NAME } from './_tree-name.mjs'
|
|
import { allAreas, areasForBundle } from './i18n-areas.mjs'
|
|
|
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
const AREAS = allAreas(ROOT)
|
|
const TREE = join(ROOT, 'public', TREE_NAME)
|
|
let bad = 0, checked = 0
|
|
for (const b of readdirSync(join(ROOT, 'plugin-designs'))) {
|
|
const js = join(TREE, b, `${b}.js`)
|
|
if (!existsSync(join(ROOT, 'plugin-designs', b, 'index.js')) || !existsSync(js)) continue
|
|
const code = readFileSync(js, 'utf8')
|
|
const included = new Set(areasForBundle(ROOT, b).areas)
|
|
const asked = AREAS.filter((a) => new RegExp('["\'`@]' + a + '\\.[A-Za-z_$]').test(code))
|
|
const missing = asked.filter((a) => !included.has(a))
|
|
checked++
|
|
if (missing.length) { bad++; console.log(` FAIL ${b}: its built code asks for ${missing.join(', ')}, which the build left out`) }
|
|
else console.log(` ok ${b.padEnd(24)} ${included.size}/${AREAS.length} areas bundled; every area its code asks for is in`)
|
|
}
|
|
if (!checked) { console.log('I18N-TREESHAKE: FAIL — no built bundles (vacuous)'); process.exit(1) }
|
|
if (bad) { console.log(`I18N-TREESHAKE: FAIL — ${bad} bundle(s) would show raw keys.`); process.exit(1) }
|
|
console.log(`I18N-TREESHAKE: PASS — ${checked} bundle(s): each carries every translation area its code asks for.`)
|