// Flags array-spread of things that are not arrays. // // npm run check:spreads // // The engine's build transpiles `[...a, ...b]` to `[].concat(a, b)`. That is // correct for arrays, but `Array.prototype.concat` does NOT expand an arbitrary // iterable — it appends it as a single element. So // // for (const view of [...this._views.values()]) // Map iterator // // silently loops once, over the iterator object itself, and every property // access on it is undefined. It type-checks, it reads perfectly, and it only // fails at runtime on a real build. That cost a debugging session; hence this // check. Use `Array.from(...)` instead, which transpiles safely. import { readdirSync, readFileSync, statSync } from 'node:fs'; import { join } from 'node:path'; const ROOT = 'assets/scripts'; /** Spreads of an iterator or collection, rather than an array. */ const HAZARDS = [ { pattern: /\.\.\.[\w.$]*\.values\(\)/, what: '.values()' }, { pattern: /\.\.\.[\w.$]*\.keys\(\)/, what: '.keys()' }, { pattern: /\.\.\.[\w.$]*\.entries\(\)/, what: '.entries()' }, { pattern: /\.\.\.new (?:Map|Set)\b/, what: 'a new Map/Set' }, { pattern: /\.\.\.[\w.$]*(?:Map|Set)\b(?!\s*\()/, what: 'something named like a Map/Set' }, ]; const offenders = []; function walk(dir) { for (const entry of readdirSync(dir)) { const path = join(dir, entry); if (statSync(path).isDirectory()) walk(path); else if (path.endsWith('.ts')) inspect(path); } } function inspect(path) { readFileSync(path, 'utf8').split('\n').forEach((line, index) => { const code = line.trim(); if (code.startsWith('*') || code.startsWith('//')) return; for (const { pattern, what } of HAZARDS) { if (pattern.test(code)) { offenders.push(`${path}:${index + 1} spreads ${what} — use Array.from(...)\n ${code}`); return; } } }); } walk(ROOT); if (offenders.length > 0) { console.error( 'Array spread of a non-array. The build turns [...x] into [].concat(x),\n' + 'which appends an iterator instead of expanding it:\n - ' + offenders.join('\n - '), ); process.exit(1); } console.log('spreads OK');