86 lines
4.9 KiB
TypeScript
86 lines
4.9 KiB
TypeScript
// docs/desktop-coverage.md from cli/lib/desktop-coverage.ts, checked against the code: a command is "covered" only
|
|
// when every method it names exists (serve.ts describeMethods(), or a #[tauri::command] of src-tauri) and the window
|
|
// calls it (the method quoted in apps/desktop/src, or listed in TOOLBOX, which the More tools tab renders).
|
|
// `bun tools/gen-desktop-coverage.ts` writes the page; `--check` exits 1 when it is stale. tests/desktop-coverage.test.ts
|
|
// runs analyze() too.
|
|
import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { COMMANDS, PLAIN } from '../cli/commands';
|
|
import { DESKTOP_COVERAGE, OWNER_STEPS, TOOLBOX } from '../cli/lib/desktop-coverage';
|
|
import { describeMethods } from '../cli/serve';
|
|
|
|
const ROOT = join(import.meta.dir, '..');
|
|
const OUT = join(ROOT, 'docs/desktop-coverage.md');
|
|
|
|
function files(dir: string, out: string[] = []): string[] {
|
|
for (const f of readdirSync(dir)) {
|
|
const p = join(dir, f);
|
|
if (statSync(p).isDirectory()) { if (f !== 'dev' && f !== 'i18n') files(p, out); } else if (/\.(ts|tsx|rs)$/.test(f)) out.push(p);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export interface Row { command: string; status: 'covered' | 'gap' | 'not-for-desktop'; where: string; methods: string[]; owner?: string; problems: string[] }
|
|
|
|
export function analyze(): { rows: Row[]; extra: string[] } {
|
|
const src = files(join(ROOT, 'apps/desktop/src')).map(f => readFileSync(f, 'utf8')).join('\n');
|
|
const rust = files(join(ROOT, 'apps/desktop/src-tauri/src')).map(f => readFileSync(f, 'utf8')).join('\n');
|
|
const hostCmds = new Set([...rust.matchAll(/#\[tauri::command\][\s\S]{0,200}?fn ([a-z_]+)/g)].map(m => m[1]!));
|
|
const methods = new Set(describeMethods().map(m => m.method));
|
|
const toolboxShown = /\bTOOLBOX\b/.test(src);
|
|
const inToolbox = new Set(TOOLBOX.flatMap(g => g.methods));
|
|
const reachable = (m: string): string | null => {
|
|
if (m.startsWith('host:')) {
|
|
const c = m.slice(5);
|
|
if (!hostCmds.has(c)) return `${m}: no such tauri command`;
|
|
return src.includes(`'${c}'`) ? null : `${m}: never invoked by the window`;
|
|
}
|
|
if (!methods.has(m)) return `${m}: no such serve method`;
|
|
if (src.includes(`'${m}'`) || src.includes(`"${m}"`)) return null;
|
|
if (toolboxShown && inToolbox.has(m)) return null;
|
|
return `${m}: no window place calls it`;
|
|
};
|
|
const all = [...Object.keys(PLAIN), ...Object.keys(COMMANDS)];
|
|
const rows: Row[] = all.map(command => {
|
|
const c = DESKTOP_COVERAGE[command];
|
|
if (!c) return { command, status: 'gap', where: '', methods: [], problems: ['no entry in cli/lib/desktop-coverage.ts'] };
|
|
if ('not' in c) return { command, status: 'not-for-desktop', where: c.not, methods: [], problems: c.not.trim() ? [] : ['not-for-desktop without a reason'] };
|
|
const problems = c.methods.map(reachable).filter((p): p is string => !!p);
|
|
if (!c.methods.length && !(c.owner && toolboxShown && OWNER_STEPS.length)) problems.push('no method and no owner step');
|
|
return { command, status: problems.length ? 'gap' : 'covered', where: c.where, methods: c.methods, ...(c.owner ? { owner: c.owner } : {}), problems };
|
|
});
|
|
return { rows, extra: Object.keys(DESKTOP_COVERAGE).filter(k => !all.includes(k)) };
|
|
}
|
|
|
|
export function render(rows: Row[]): string {
|
|
const n = (s: Row['status']): number => rows.filter(r => r.status === s).length;
|
|
const cell = (s: string): string => s.replace(/\|/g, '\\|');
|
|
return [
|
|
'# Desktop coverage',
|
|
'',
|
|
'Generated by `bun tools/gen-desktop-coverage.ts` from `cli/lib/desktop-coverage.ts`; do not edit by hand.',
|
|
'Every CLI command (cli/commands.ts) and the desktop place that runs it. "covered" means each method listed exists',
|
|
'(serve.ts, or a host command `host:`) and the window calls it; tests/desktop-coverage.test.ts fails on a gap.',
|
|
'Owner-only parts are owner actions in the window or owner steps with their terminal command (More tools).',
|
|
'',
|
|
`**${rows.length} commands:** ${n('covered')} covered, ${n('gap')} gaps, ${n('not-for-desktop')} not for the desktop.`,
|
|
'',
|
|
'| command | status | where in the window | methods | owner step |',
|
|
'|---|---|---|---|---|',
|
|
...rows.map(r => `| \`${r.command}\` | ${r.status} | ${cell(r.where || r.problems.join('; '))} | ${r.methods.map(m => `\`${m}\``).join(', ')} | ${cell(r.owner ?? '')} |`),
|
|
'',
|
|
].join('\n');
|
|
}
|
|
|
|
if (import.meta.main) {
|
|
const { rows } = analyze();
|
|
const md = render(rows);
|
|
if (process.argv.includes('--check')) {
|
|
let cur = '';
|
|
try { cur = readFileSync(OUT, 'utf8'); } catch { /* missing */ }
|
|
if (cur !== md) { console.error('docs/desktop-coverage.md is stale: run bun tools/gen-desktop-coverage.ts'); process.exit(1); }
|
|
} else writeFileSync(OUT, md);
|
|
const gaps = rows.filter(r => r.status === 'gap');
|
|
for (const g of gaps) console.log(`gap: ${g.command}: ${g.problems.join('; ')}`);
|
|
console.log(`${rows.length} commands, ${rows.filter(r => r.status === 'covered').length} covered, ${gaps.length} gaps`);
|
|
}
|