Archify Diagram Viewer 0.1.0
VS Code extension that previews Archify diagrams from their JSON sources (live, as you type) and opens rendered Archify HTML in a viewer tab. Bundles the Archify 3.0.1 renderer and runs it on VS Code's Node runtime. Adds validation diagnostics, JSON schema help, source-link navigation, export saving, render-to-file and open-in-browser commands. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+195
@@ -0,0 +1,195 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { DiagramSource, DiagramType, hasSourceEvidence, stripSourceEvidence } from './detect';
|
||||
|
||||
export interface RuntimeOptions {
|
||||
/** Absolute path to vendor/archify/bin/archify.mjs. */
|
||||
cliPath: string;
|
||||
/** Node (or Electron) executable. */
|
||||
nodePath: string;
|
||||
/** Set ELECTRON_RUN_AS_NODE so VS Code's Electron binary behaves as Node. */
|
||||
runAsNode: boolean;
|
||||
/** Scratch directory for input/output files. */
|
||||
workDir: string;
|
||||
quality?: string;
|
||||
repoRoot?: string;
|
||||
timeoutMs?: number;
|
||||
log?: (line: string) => void;
|
||||
}
|
||||
|
||||
export interface ArchifyDiagnostic {
|
||||
code?: string;
|
||||
severity?: 'error' | 'warning' | 'info';
|
||||
message: string;
|
||||
subject?: { path?: string; identity?: string; [key: string]: unknown };
|
||||
supportedFixes?: string[];
|
||||
}
|
||||
|
||||
export interface ValidateResult {
|
||||
ok: boolean;
|
||||
diagnostics: ArchifyDiagnostic[];
|
||||
/** Set when the CLI did not produce parseable JSON. */
|
||||
crash?: string;
|
||||
}
|
||||
|
||||
export interface RenderResult {
|
||||
ok: boolean;
|
||||
html?: string;
|
||||
error?: string;
|
||||
/** Set when the diagram rendered without its source evidence. */
|
||||
evidenceNotice?: string;
|
||||
}
|
||||
|
||||
interface RunResult {
|
||||
code: number | null;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
}
|
||||
|
||||
const EVIDENCE_ERROR_RE = /repository-evidence\/|source evidence|--repo-root|Evidence (root|repository|revision)/i;
|
||||
|
||||
function run(options: RuntimeOptions, args: string[], env: Record<string, string> = {}): Promise<RunResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
options.log?.(`archify ${args.join(' ')}`);
|
||||
const child = spawn(options.nodePath, [options.cliPath, ...args], {
|
||||
cwd: options.workDir,
|
||||
env: {
|
||||
...process.env,
|
||||
...(options.runAsNode ? { ELECTRON_RUN_AS_NODE: '1' } : {}),
|
||||
...env,
|
||||
},
|
||||
windowsHide: true,
|
||||
});
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
child.stdout.setEncoding('utf8').on('data', (chunk: string) => (stdout += chunk));
|
||||
child.stderr.setEncoding('utf8').on('data', (chunk: string) => (stderr += chunk));
|
||||
const timer = setTimeout(() => child.kill(), options.timeoutMs ?? 60_000);
|
||||
child.on('error', (error) => {
|
||||
clearTimeout(timer);
|
||||
reject(error);
|
||||
});
|
||||
child.on('close', (code) => {
|
||||
clearTimeout(timer);
|
||||
if (stderr.trim()) options.log?.(stderr.trim());
|
||||
resolve({ code, stdout, stderr });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function commonArgs(options: RuntimeOptions, withRepoRoot: boolean): string[] {
|
||||
const args: string[] = [];
|
||||
if (options.quality) args.push('--quality', options.quality);
|
||||
if (withRepoRoot && options.repoRoot) args.push('--repo-root', options.repoRoot);
|
||||
return args;
|
||||
}
|
||||
|
||||
async function withScratch<T>(options: RuntimeOptions, body: (dir: string) => Promise<T>): Promise<T> {
|
||||
await fs.mkdir(options.workDir, { recursive: true });
|
||||
const dir = await fs.mkdtemp(path.join(options.workDir, 'run-'));
|
||||
try {
|
||||
return await body(dir);
|
||||
} finally {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
export async function validate(options: RuntimeOptions, type: DiagramType, text: string): Promise<ValidateResult> {
|
||||
return withScratch(options, async (dir) => {
|
||||
const input = path.join(dir, `diagram.${type}.json`);
|
||||
await fs.writeFile(input, text, 'utf8');
|
||||
const result = await run(options, ['validate', type, input, '--json', ...commonArgs(options, true)]);
|
||||
try {
|
||||
const report = JSON.parse(result.stdout);
|
||||
const diagnostics: ArchifyDiagnostic[] = Array.isArray(report.diagnostics) ? report.diagnostics : [];
|
||||
if (!report.ok && diagnostics.length === 0 && report.error) {
|
||||
diagnostics.push({ severity: 'error', message: String(report.error) });
|
||||
}
|
||||
return { ok: Boolean(report.ok), diagnostics };
|
||||
} catch {
|
||||
const crash = (result.stderr || result.stdout || `archify exited with code ${result.code}`).trim();
|
||||
return { ok: false, diagnostics: [], crash };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function renderOnce(
|
||||
options: RuntimeOptions,
|
||||
type: DiagramType,
|
||||
text: string,
|
||||
withRepoRoot: boolean,
|
||||
): Promise<RenderResult> {
|
||||
return withScratch(options, async (dir) => {
|
||||
const input = path.join(dir, `diagram.${type}.json`);
|
||||
const output = path.join(dir, 'diagram.html');
|
||||
await fs.writeFile(input, text, 'utf8');
|
||||
const result = await run(options, ['render', type, input, output, ...commonArgs(options, withRepoRoot)]);
|
||||
if (result.code === 0) {
|
||||
try {
|
||||
return { ok: true, html: await fs.readFile(output, 'utf8') };
|
||||
} catch {
|
||||
// fall through to the error below
|
||||
}
|
||||
}
|
||||
const error = (result.stderr || result.stdout || `archify exited with code ${result.code}`).trim();
|
||||
return { ok: false, error };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a diagram to HTML. With `evidence: 'fallback'`, a diagram whose
|
||||
* source evidence cannot be verified is rendered again without it, and the
|
||||
* reason is returned as `evidenceNotice`.
|
||||
*/
|
||||
export async function renderDiagram(
|
||||
options: RuntimeOptions,
|
||||
source: DiagramSource & { text: string },
|
||||
evidence: 'fallback' | 'strict',
|
||||
): Promise<RenderResult> {
|
||||
const withEvidence = hasSourceEvidence(source.type, source.diagram);
|
||||
if (withEvidence && evidence === 'fallback' && !options.repoRoot) {
|
||||
const stripped = JSON.stringify(stripSourceEvidence(source.type, source.diagram), null, 2);
|
||||
const result = await renderOnce(options, source.type, stripped, false);
|
||||
return {
|
||||
...result,
|
||||
evidenceNotice: 'Source links are hidden: the diagram is not inside a Git checkout, so its source evidence cannot be verified. Set "archify.repoRoot" to the matching checkout.',
|
||||
};
|
||||
}
|
||||
const first = await renderOnce(options, source.type, source.text, withEvidence);
|
||||
if (first.ok || !withEvidence || evidence !== 'fallback' || !isEvidenceError(first.error)) {
|
||||
return first;
|
||||
}
|
||||
const stripped = JSON.stringify(stripSourceEvidence(source.type, source.diagram), null, 2);
|
||||
const second = await renderOnce(options, source.type, stripped, false);
|
||||
if (!second.ok) return first;
|
||||
const reason = (first.error ?? '').split('\n').find((line) => line.trim()) ?? 'verification failed';
|
||||
return { ...second, evidenceNotice: `Source links are hidden: ${reason.replace(/^\[[^\]]+\]\s*/, '')}` };
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders to a file the user chose. Source evidence is verified strictly
|
||||
* unless `stripEvidence` is set, in which case it is removed first.
|
||||
*/
|
||||
export async function renderToFile(
|
||||
options: RuntimeOptions,
|
||||
source: DiagramSource & { text: string },
|
||||
outputPath: string,
|
||||
stripEvidence = false,
|
||||
): Promise<RenderResult> {
|
||||
const text = stripEvidence
|
||||
? JSON.stringify(stripSourceEvidence(source.type, source.diagram), null, 2)
|
||||
: source.text;
|
||||
const withEvidence = !stripEvidence && hasSourceEvidence(source.type, source.diagram);
|
||||
return withScratch(options, async (dir) => {
|
||||
const input = path.join(dir, `diagram.${source.type}.json`);
|
||||
await fs.writeFile(input, text, 'utf8');
|
||||
const result = await run(options, ['render', source.type, input, outputPath, ...commonArgs(options, withEvidence)]);
|
||||
if (result.code === 0) return { ok: true };
|
||||
return { ok: false, error: (result.stderr || result.stdout || `archify exited with code ${result.code}`).trim() };
|
||||
});
|
||||
}
|
||||
|
||||
export function isEvidenceError(error: string | undefined): boolean {
|
||||
return EVIDENCE_ERROR_RE.test(error ?? '');
|
||||
}
|
||||
Reference in New Issue
Block a user