Add New Diagram command with starter templates

- Archify: New Diagram… (Explorer folder menu, File > New File…, Command
  Palette) creates a .archify file from a template and opens the diagram
- Six YAML templates: architecture, workflow, sequence, dataflow,
  lifecycle and an everyday plan; connections carry ids, workflows use
  schema 2
- Title and meta.output follow the file name; template comments are kept
- Tests: every template passes archify validate; integration check for
  the command

Closes #2
This commit is contained in:
2026-10-04 21:33:11 +03:00
parent d2f52d43ac
commit 9dc1c5acc5
13 changed files with 582 additions and 1 deletions
+29
View File
@@ -97,6 +97,35 @@ export async function run(): Promise<void> {
assert.ok(diagnostics[0].range.start.line >= 2, `reported on line ${diagnostics[0].range.start.line + 1}`);
});
await check('New Diagram creates a .archify file from a template and opens the diagram', async () => {
const folder = workspaceFile('.');
// Stub the template pick and the name prompt; check the name validation on the way.
const window = vscode.window as unknown as Record<string, unknown>;
const originalPick = window.showQuickPick;
const originalInput = window.showInputBox;
let existingNameMessage: unknown;
window.showQuickPick = async (items: readonly { label: string }[]) => (await items).find((item) => item.label === 'Everyday Plan');
window.showInputBox = async (options: vscode.InputBoxOptions) => {
existingNameMessage = await options.validateInput?.('web-platform');
return 'summer-plan';
};
try {
await vscode.commands.executeCommand('archify.newDiagram', folder);
} finally {
window.showQuickPick = originalPick;
window.showInputBox = originalInput;
}
assert.match(String(existingNameMessage), /already exists/);
const uri = vscode.Uri.joinPath(folder, 'summer-plan.archify');
const text = new TextDecoder().decode(await vscode.workspace.fs.readFile(uri));
assert.match(text, /^diagram_type: workflow$/m);
assert.match(text, /^ {2}title: Summer plan$/m);
assert.match(text, /^ {2}output: summer-plan\.html$/m);
await waitFor('diagram editor for the new file', () =>
webviewTabs('archify.diagram').some((tab) => tab.input instanceof vscode.TabInputCustom && tab.input.uri.toString() === uri.toString()),
);
});
await check('opens a live preview beside the source', async () => {
await vscode.commands.executeCommand('archify.showPreviewToSide', workspaceFile('production-deployment.architecture.json'));
const tabs = await waitFor('preview tab', () => {
+64
View File
@@ -0,0 +1,64 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { test } from 'node:test';
import { parse } from 'yaml';
import { readDiagramSource } from '../src/detect';
import { validate } from '../src/renderer';
import { diagramFromTemplate, diagramName, invalidNameReason, TEMPLATES, titleFromName } from '../src/templates';
const root = path.resolve(__dirname, '..', '..');
const templates = path.join(root, 'templates');
const runtime = {
cliPath: path.join(root, 'vendor', 'archify', 'bin', 'archify.mjs'),
nodePath: process.execPath,
runAsNode: false,
workDir: fs.mkdtempSync(path.join(os.tmpdir(), 'archify-ext-test-')),
};
test('every template file is listed and every listed template exists', () => {
const files = fs.readdirSync(templates).filter((file) => file.endsWith('.archify')).sort();
assert.deepEqual(TEMPLATES.map((item) => item.file).sort(), files);
});
for (const item of TEMPLATES) {
test(`template ${item.file} is a valid ${item.type} diagram`, async () => {
const text = diagramFromTemplate(fs.readFileSync(path.join(templates, item.file), 'utf8'), item.defaultName);
const source = readDiagramSource(text, `${item.defaultName}.archify`);
assert.ok(source);
assert.equal(source.type, item.type);
assert.equal(source.format, 'yaml');
const result = await validate(runtime, source.type, source.json);
assert.deepEqual(
result.diagnostics.filter((d) => d.severity === 'error').map((d) => `${d.code}: ${d.message}`),
[],
);
assert.ok(result.ok);
});
}
test('diagramFromTemplate sets title and output and keeps comments and flow lists', () => {
const text = diagramFromTemplate('# keep me\nschema_version: 1\nmeta:\n title: Old\n output: old.html\npos: [1, 2]\n', 'sales-report');
assert.match(text, /^# keep me/);
assert.match(text, /pos: \[1, 2\]/);
assert.deepEqual(parse(text).meta, { title: 'Sales report', output: 'sales-report.html' });
});
test('diagramFromTemplate adds meta when the template has none', () => {
assert.deepEqual(parse(diagramFromTemplate('diagram_type: workflow\n', 'plan')).meta, { title: 'Plan', output: 'plan.html' });
});
test('diagram names', () => {
assert.equal(diagramName(' web.archify '), 'web');
assert.equal(diagramName('web.ARCHIFY'), 'web');
assert.equal(invalidNameReason('web'), undefined);
assert.equal(invalidNameReason('my plan 2026'), undefined);
assert.ok(invalidNameReason(''));
assert.ok(invalidNameReason('.archify'));
assert.ok(invalidNameReason('..'));
assert.ok(invalidNameReason('a/b'));
assert.ok(invalidNameReason('a:b'));
assert.equal(titleFromName('sales-report_v2'), 'Sales report v2');
assert.equal(titleFromName('plan'), 'Plan');
});