#!/usr/bin/env node /** * Fail-loud check for stale ecosystem aliases and old `App.` * references in `src/`. The point is to keep documentation honest after * the rename to full-word aliases (`$cache` / `$session` / etc.) and * the move from capitalized service slots (`App.Cache`) to lowercase * declarable services (`App.cache`). * * Allowed capitalised names on `App.*` are the ecosystem core: * `Logger`, `Bus`, `Timers`, `Orca`. Everything else is lowercase. * * Run: `node scripts/check-aliases.mjs` * Exit code: 0 when clean, 1 when any forbidden token is found. */ import { readFileSync, readdirSync, statSync } from 'node:fs'; import { join, relative, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; const HERE = fileURLToPath(new URL('.', import.meta.url)); const ROOT = join(HERE, '..'); const SCAN_ROOTS = [join(ROOT, 'src'), join(ROOT, 'scripts')]; const STALE_ALIAS_PATTERNS = [ { rx: /\$cach[^e]/g, hint: '$cach* → $cache*' }, { rx: /\$sess[^i]/g, hint: '$sess* → $session*' }, { rx: /\$conn[^e]/g, hint: '$conn* → $connection*' }, { rx: /\$timr\b/g, hint: '$timr → $timer' }, { rx: /\$logr\b/g, hint: '$logr → $logger' }, { rx: /\$fmts\b/g, hint: '$fmts → $format' }, { rx: /\$stor[^a]/g, hint: '$stor* → $storage*' }, { rx: /\$aapp\b/g, hint: '$aapp → $active-app' }, { rx: /\$buss\b/g, hint: '$buss → $bus' }, { rx: /\$libs\/buss\b/g, hint: '$libs/buss → $libs/bus' }, { rx: /\$libs\/logr\b/g, hint: '$libs/logr → $libs/logger' }, { rx: /\$libs\/timr\b/g, hint: '$libs/timr → $libs/timer' }, { rx: /\$libs\/cach[^e]/g, hint: '$libs/cach* → $libs/cache*' } ]; const APP_FORBIDDEN_CAPITALS = [ 'Cache', 'Sess', 'Session', 'Sium', 'Storage', 'Format', 'Frontend', 'Lang', 'Auth', 'Perm', 'Perms', 'Permissions', 'Http', 'Dom', 'Connections', 'Prefs' ]; const APP_PATTERN = new RegExp(`\\bApp\\.(${APP_FORBIDDEN_CAPITALS.join('|')})\\b`, 'g'); /** * Pre-`createActiveApp({ services })` API surfaces. The current runtime * routes locale through `App.lang` / `App.prefs` and validation * through `App.sium`; these old method handles no longer exist on the * App proxy and call sites must be migrated. The audit explicitly * flagged that the previous version of this guard missed them. */ const OLD_APP_METHOD_PATTERNS = [ { rx: /\bApp\.setLocale\b/g, hint: 'App.setLocale → App.lang.setLocale (or App.prefs.setIntent("language", ...))' }, { rx: /\bApp\.getLocale\b/g, hint: 'App.getLocale → App.lang.getLocale (or App.prefs.effective().language)' }, { rx: /\bApp\.createSiumEngine\b/g, hint: 'App.createSiumEngine → declare `sium: defineEngineSium()` in services and read App.sium' } ]; const SKIP_DIRS = new Set(['node_modules', '.svelte-kit', '.git', 'build', 'dist']); const SCAN_EXTENSIONS = new Set([ '.ts', '.svelte', '.svelte.ts', '.js', '.mjs', '.md', '.txt' ]); /** * Files where the references are deliberately historical or where the * tokens are scanner patterns rather than real call sites. Paths are * relative to the repo root so both `src/` and `scripts/` entries fit. */ const ALLOWLIST = new Set([ join('src', 'arts', 'active-app', 'types.ts').replaceAll('/', sep), // The scanner itself encodes every forbidden token as a regex — // matching against its own source is a false positive by definition. join('scripts', 'check-aliases.mjs').replaceAll('/', sep), // Legacy demo pages still using the pre-service-schema App methods. // Each one already opts out of static prerender via a sibling // `+page.ts`; they stay in the tree as dev-only references until // migrated to `createActiveApp({ services })`. Removing the // allowlist entry without migrating the page will fail this gate. join('src', 'web', 'routes', 'test', 'aapp', '+page.svelte').replaceAll('/', sep), join('src', 'web', 'routes', 'test', 'ecosystem', '+page.svelte').replaceAll('/', sep), join('src', 'web', 'routes', 'test', 'http', '+page.svelte').replaceAll('/', sep) ]); function* walk(dir) { for (const entry of readdirSync(dir)) { if (SKIP_DIRS.has(entry)) continue; const full = join(dir, entry); const st = statSync(full); if (st.isDirectory()) { yield* walk(full); continue; } if (!st.isFile()) continue; const lower = entry.toLowerCase(); if (!Array.from(SCAN_EXTENSIONS).some((ext) => lower.endsWith(ext))) continue; yield full; } } function scanFile(file) { const text = readFileSync(file, 'utf8'); const findings = []; for (const { rx, hint } of STALE_ALIAS_PATTERNS) { rx.lastIndex = 0; const matches = text.match(rx); if (matches) findings.push({ kind: 'alias', hint, count: matches.length }); } APP_PATTERN.lastIndex = 0; const appMatches = text.match(APP_PATTERN); if (appMatches) { findings.push({ kind: 'app', hint: `App. required for non-core services (${[...new Set(appMatches)].join(', ')})`, count: appMatches.length }); } for (const { rx, hint } of OLD_APP_METHOD_PATTERNS) { rx.lastIndex = 0; const matches = text.match(rx); if (matches) findings.push({ kind: 'old-api', hint, count: matches.length }); } return findings; } let exit = 0; const offenders = []; for (const root of SCAN_ROOTS) { for (const file of walk(root)) { const rel = relative(ROOT, file); if (ALLOWLIST.has(rel)) continue; const findings = scanFile(file); if (findings.length === 0) continue; offenders.push({ rel, findings }); exit = 1; } } if (offenders.length === 0) { console.log('aliases: clean'); } else { console.log('aliases: found stale references'); for (const { rel, findings } of offenders) { console.log(` ${rel}`); for (const f of findings) console.log(` - ${f.hint} (x${f.count})`); } console.log( '\nFix: run a search/replace using the canonical aliases in svelte.config.js' ); } process.exit(exit);