// node --test tools/symbols/check-no-nc.test.mjs — planted files. import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; import { randomBytes } from 'node:crypto'; import { copyFileSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import test from 'node:test'; import { gzipSync, brotliCompressSync } from 'node:zlib'; import { check, PACK_MAGIC } from './check-no-nc.mjs'; function site(files, prefix = '') { const dir = mkdtempSync(join(tmpdir(), 'check-no-nc-')); const base = { 'index.html': 'AAC', 'pkg/aac-web.0123456789ab.js': 'export default 1;', 'sw.js': `const PRECACHE_URLS = ["/","/pkg/aac-web.0123456789ab.js"];`, 'voices/c/g2p/it/dict.tsv': 'sclera\tˈsklɛra\n', 'voices/c/g2p/fr/verb.csv': 'musclera,muscler\n', 'models/lang/it.0123456789ab.bin': 'sclera scleral', }; // The usual files sit under the app's prefix; planted ones where they say. const all = { ...Object.fromEntries(Object.entries(base).map(([k, v]) => [prefix + k, v])), ...files }; for (const [path, content] of Object.entries(all)) { const p = join(dir, path); mkdirSync(dirname(p), { recursive: true }); writeFileSync(p, content); } return dir; } const clean = { packs: [], named: [], precached: [] }; test('a clean build passes, whatever the third-party dictionaries hold', () => { for (const prefix of ['', 'app/']) { const dir = site({}, prefix); assert.deepEqual(check(dir, prefix), clean); rmSync(dir, { recursive: true }); } }); test('a file carrying a pack\'s bytes is caught, whatever its name or place', () => { const dir = site({ 'pkg/x.bin': Buffer.concat([PACK_MAGIC, Buffer.from('rest')]), 'voices/y.dat': PACK_MAGIC }); assert.deepEqual(check(dir).packs.sort(), ['pkg/x.bin', 'voices/y.dat']); rmSync(dir, { recursive: true }); }); test('a pack compressed as a .br or .gz sidecar is caught in every folder, third-party ones too', () => { // Incompressible, so each sidecar is larger than the part of it first read. const pack = Buffer.concat([PACK_MAGIC, randomBytes(200_000)]); const dir = site({ 'voices/c/g2p/it/z.tsv.br': brotliCompressSync(pack), 'models/lang/w.bin.gz': gzipSync(pack), 'pkg/blob.bin.br': brotliCompressSync(pack), 'vendor/v.js.gz': gzipSync(pack), // Compressed, but not a pack. 'voices/c/g2p/it/ok.tsv.br': brotliCompressSync('ciao\tˈtʃao\n'), 'pkg/ok.js.gz': gzipSync('export default 2;'), // Named like a sidecar, not compressed, and not a pack. 'ai/raw.br': 'not brotli', }); assert.deepEqual(check(dir).packs.sort(), ['models/lang/w.bin.gz', 'pkg/blob.bin.br', 'vendor/v.js.gz', 'voices/c/g2p/it/z.tsv.br']); rmSync(dir, { recursive: true }); }); test('anything under models/symbols/, or any *.pack, is caught', () => { const dir = site({ 'models/symbols/LICENSE.md': 'licence', 'pkg/a.pack': 'x', 'ai/b.pack.gz': 'x' }); assert.deepEqual(check(dir).packs.sort(), ['ai/b.pack.gz', 'models/symbols/LICENSE.md', 'pkg/a.pack']); rmSync(dir, { recursive: true }); }); test('the service worker precaching a pack is caught', () => { const dir = site({ 'sw.js': 'const PRECACHE_URLS = ["/","/models/symbols/x.0123456789ab.pack"];' }); assert.deepEqual(check(dir).precached, ['/models/symbols/x.0123456789ab.pack']); rmSync(dir, { recursive: true }); }); test('a worker whose precache list cannot be read fails', () => { const dir = site({ 'sw.js': 'const PRECACHE_URLS = __PRECACHE_URLS__;' }); assert.throws(() => check(dir), /no PRECACHE_URLS list/); rmSync(dir, { recursive: true }); }); test('an own file naming a set is caught, sidecars decompressed', () => { const dir = site({ 'pkg/aac-web.0123456789ab.wasm': 'xx ARASAAC yy', 'symbols/LICENSES.md': '# Sclera', 'index.html.gz': gzipSync('

arasaac

'), 'pkg/s.js.br': brotliCompressSync('sclera'), }); assert.deepEqual(check(dir).named.sort(), ['index.html.gz', 'pkg/aac-web.0123456789ab.wasm', 'pkg/s.js.br', 'symbols/LICENSES.md']); rmSync(dir, { recursive: true }); }); test('a file named for a set is caught in every folder, third-party ones too', () => { const dir = site({ 'vendor/arasaac-loader.js': 'export {};', 'voices/c/Sclera.txt': '', 'models/lang/arasaac/w.bin': 'x', 'pkg/sclera.js': 'export {};', }); assert.deepEqual(check(dir).named.sort(), ['models/lang/arasaac/w.bin', 'pkg/sclera.js', 'vendor/arasaac-loader.js', 'voices/c/Sclera.txt']); rmSync(dir, { recursive: true }); }); test('under /app/, the third-party folders are the app\'s', () => { const dir = site({ 'app/pkg/x.js': 'sclera', 'app/voices/y.txt': 'sclera' }, 'app/'); assert.deepEqual(check(dir, 'app/').named, ['app/pkg/x.js']); // The same folders outside the app are not third-party data. const other = site({ 'voices/x.txt': 'sclera' }, 'app/'); assert.deepEqual(check(other, 'app/').named, ['voices/x.txt']); rmSync(dir, { recursive: true }); rmSync(other, { recursive: true }); }); test('run as a command, as prerender-static.sh does, from any path, it checks', () => { const script = fileURLToPath(new URL('./check-no-nc.mjs', import.meta.url)); const tmp = mkdtempSync(join(tmpdir(), 'check-no-nc-cli-')); const spaced = join(tmp, 'with space'); mkdirSync(spaced); copyFileSync(script, join(spaced, 'check-no-nc.mjs')); symlinkSync(dirname(script), join(tmp, 'linked')); const ways = [ { name: 'as prerender-static.sh', args: [script] }, { name: 'from a path with a space', args: [join(spaced, 'check-no-nc.mjs')] }, { name: 'through a symlinked directory', args: [join(tmp, 'linked', 'check-no-nc.mjs')] }, { name: 'relative, inside the spaced path', args: ['check-no-nc.mjs'], cwd: spaced }, ]; const planted = site({ 'index.html': '

ARASAAC

' }); const clean = site({}); for (const way of ways) { const run = (dir) => spawnSync(process.execPath, [...way.args, dir], { cwd: way.cwd, encoding: 'utf8' }); const bad = run(planted); assert.equal(bad.status, 1, `${way.name}: ${bad.stdout}${bad.stderr}`); assert.match(bad.stderr, /names them in 1 file\(s\):\n index\.html/, way.name); const good = run(clean); assert.equal(good.status, 0, `${way.name}: ${good.stdout}${good.stderr}`); assert.match(good.stdout, /✓ no symbol pack/, way.name); } for (const dir of [tmp, planted, clean]) rmSync(dir, { recursive: true }); });