Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b536f3e10 | ||
|
|
bc294dc6bd | ||
|
|
1a5482b4bf | ||
|
|
ac9200ccdd | ||
|
|
9871f774f4 | ||
|
|
ca3f08bac9 | ||
|
|
86f6d5c668 | ||
|
|
789ccb2d5c | ||
|
|
adc5cdbe97 | ||
|
|
e9da971523 | ||
|
|
8d8fc6f65e | ||
|
|
6f6ffae96f | ||
|
|
f102e722ec | ||
|
|
3be676f3b2 | ||
|
|
9357176673 |
No files matched your search
@@ -1,5 +1,29 @@
|
||||
# Changelog
|
||||
|
||||
## 0.5.2 — 2026-10-10
|
||||
|
||||
- **Right-click menus:** the Archify commands (Archify | New Diagram…, Archify | Open Preview, Archify | Render to HTML File…, Archify | Open in HTML Viewer) now sit in their own section at the very bottom of the Explorer and editor-tab menus, instead of between New File, New Folder and the other built-in entries.
|
||||
- **All commands are named "Archify | …"**, the same in right-click menus, the editor title bar and the Command Palette: Archify | New Diagram… (was *New Diagram Archify…*), Archify | Open Preview, Archify | Open Preview to the Side, Archify | Render to HTML File…, Archify | Open in HTML Viewer, Archify | Open in Browser, Archify | Refresh Preview, Archify | Show Source.
|
||||
|
||||
## 0.5.1 — 2026-10-07
|
||||
|
||||
- **Clearer descriptions:** the Marketplace description, README and messages now say plainly that diagrams are `.archify` files in YAML. Archify's own JSON files (`*.architecture.json`, …) and rendered HTML are described as also supported, in their own section.
|
||||
- The rendered-HTML editor is now called **Archify HTML Viewer** (was *Archify Diagram Viewer*, the same name as the extension), and its command **Open in Archify HTML Viewer**.
|
||||
- Messages: "open a .archify diagram first"; an empty or broken `.archify` file asks for valid YAML, not JSON.
|
||||
|
||||
## 0.5.0 — 2026-10-04
|
||||
|
||||
- **Brand completion:** after `brand:` in `.archify` files and Archify JSON sources, the editor suggests the 107 built-in brand IDs with their product names. Aliases and product names match too (`kafka` → `apache-kafka`, `postgres` → `postgresql`).
|
||||
- **Unknown brands are underlined on their own line.** Archify reports these without a location, so they used to appear at the top of the file.
|
||||
|
||||
## 0.4.1 — 2026-10-04
|
||||
|
||||
- The new-diagram command is now titled **New Diagram Archify…** everywhere, including the Explorer folder menu, where it read *New Diagram…*.
|
||||
|
||||
## 0.4.0 — 2026-10-04
|
||||
|
||||
- **New Diagram…** (Explorer folder menu, *File → New File…*, Command Palette): pick Architecture, Workflow, Sequence, Data Flow, Lifecycle or Everyday Plan, enter a name, and a small valid `.archify` (YAML) file is created from a template and opened as a diagram. Title and output file follow the name.
|
||||
|
||||
## 0.3.0 — 2026-09-30
|
||||
|
||||
- **`.archify` files are YAML.** They are parsed as YAML and handed to the renderer as JSON. Older JSON `.archify` files still open, since JSON is valid YAML.
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
# Archify Diagram Viewer
|
||||
|
||||
View [Archify](https://github.com/tt-a1i/archify) diagrams inside VS Code. Click a `.archify` file (YAML) and the interactive diagram opens straight away; open its source beside it and the diagram follows your edits. Rendered Archify HTML files open in a viewer tab instead of a browser.
|
||||
Create and view [Archify](https://github.com/tt-a1i/archify) diagrams inside VS Code. You write a diagram as a **`.archify` file in YAML**; clicking it opens the interactive diagram, and the diagram follows your edits as you type.
|
||||
|
||||

|
||||
|
||||
This is an unofficial extension. It bundles the Archify renderer (MIT, © tt-a1i) and runs it on VS Code's own Node runtime, so nothing else needs to be installed.
|
||||
|
||||
## Supported files
|
||||
|
||||
| File | Format | What the extension does |
|
||||
|---|---|---|
|
||||
| **`name.archify`** | **YAML** (the main format) | Opens as the interactive diagram. Show Source, live updates, validation, completion, templates. |
|
||||
| `name.architecture.json`, `name.workflow.json`, `name.sequence.json`, `name.dataflow.json`, `name.lifecycle.json` | JSON (Archify's own CLI format) | Opens as text, with a preview beside it and validation. For files produced by the Archify CLI or AI agents. |
|
||||
| Rendered Archify `.html` | HTML | Read-only viewer instead of a browser. |
|
||||
|
||||
New diagrams should be `.archify` files; everything below describes them unless it says otherwise.
|
||||
|
||||
## Quick start
|
||||
|
||||
The fastest start: right-click a folder in the Explorer and choose **Archify | New Diagram…** (also in *File → New File…* and the Command Palette). Pick a kind of diagram (Architecture, Workflow, Sequence, Data Flow, Lifecycle or Everyday Plan), give it a name, and a small working `.archify` file is created and opened. Change its YAML to fit your case.
|
||||
|
||||
To write one by hand:
|
||||
|
||||
1. Create a file ending in `.archify`, for example `hello.archify`:
|
||||
|
||||
```yaml
|
||||
@@ -32,29 +46,35 @@ The same file is in [examples/hello.archify](examples/hello.archify).
|
||||
|
||||
## The `.archify` format
|
||||
|
||||
A `.archify` file is an Archify diagram source written in **YAML**. It has the same fields as the JSON the Archify CLI renders, and plain JSON content still works, since JSON is valid YAML. `diagram_type` selects the kind of diagram: `architecture`, `workflow`, `sequence`, `dataflow` or `lifecycle`.
|
||||
A `.archify` file is an Archify diagram written in **YAML**. Its fields are those of Archify's diagram schema; the extension converts the YAML for the renderer, so you never handle JSON yourself. `diagram_type` selects the kind of diagram: `architecture`, `workflow`, `sequence`, `dataflow` or `lifecycle`.
|
||||
|
||||
- **Opening:** the **Archify Diagram** editor is the default for `.archify`. Use *Reopen Editor With… → Text Editor* to edit the YAML full-screen.
|
||||
- **Completion:** with the [YAML extension](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml) (`redhat.vscode-yaml`) installed, `.archify` files get completion and hover from Archify's schemas. The schema follows `diagram_type`.
|
||||
|
||||
Archify's own JSON naming, `name.architecture.json`, `name.workflow.json` and so on, works too. Those open as text with a preview button, and get schema help from VS Code's built-in JSON support.
|
||||
`.archify` files from extension version 0.2.0 were written in JSON. They still open, because JSON is valid YAML, but new files should use YAML.
|
||||
|
||||
## Archify's JSON files
|
||||
|
||||
The Archify CLI and the AI agents that use it write diagrams as JSON, named `name.architecture.json`, `name.workflow.json` and so on. The extension supports these too: they open as text with a preview button in the editor title (<kbd>Cmd/Ctrl+K V</kbd> to the side, <kbd>Cmd/Ctrl+Shift+V</kbd> in place), get validation in Problems, and get schema completion from VS Code's built-in JSON support. Any other `.json` file that declares an Archify `diagram_type` previews the same way.
|
||||
|
||||
## Features
|
||||
|
||||
- **New diagram from a template**: *Archify | New Diagram…* creates a starter `.archify` file for any of the five diagram kinds, or an everyday plan, in the chosen folder.
|
||||
- **Diagram editor**: `.archify` files open as the rendered diagram. **Show Source** opens the YAML beside it, and edits re-render live.
|
||||
- **Live preview**: for Archify's `*.architecture.json`-style files, or any JSON with a `diagram_type`. Use the preview button in the editor title, <kbd>Cmd/Ctrl+K V</kbd> (to the side) or <kbd>Cmd/Ctrl+Shift+V</kbd>. The preview keeps the focus and route you selected (`#focus=…`, `#route=…`) when it re-renders.
|
||||
- **HTML viewer**: right-click a rendered `.html` file and choose **Open in Archify Viewer**, or use *Reopen Editor With… → Archify Diagram Viewer*. For Archify HTML already open as text, a button appears in the editor title. The viewer reloads when the file changes on disk.
|
||||
- **Problems**: YAML syntax errors, plus `archify validate`'s schema, layout and evidence problems, appear at the matching line of the source.
|
||||
- **Schema help**: completion and hover come from Archify's own schemas: built in for the JSON files, and through the YAML extension for `.archify` files.
|
||||
- **Live updates**: the diagram re-renders as you edit, and keeps the focus and route you selected (`#focus=…`, `#route=…`).
|
||||
- **HTML viewer**: right-click a rendered `.html` file and choose **Archify | Open in HTML Viewer**, or use *Reopen Editor With… → Archify HTML Viewer*. For Archify HTML already open as text, a button appears in the editor title. The viewer reloads when the file changes on disk.
|
||||
- **Problems**: YAML syntax errors, plus `archify validate`'s schema, layout and evidence problems, appear at the matching line of the `.archify` file.
|
||||
- **Brand logos**: after `brand:` the editor suggests Archify's built-in brand IDs (PostgreSQL, Redis, Kafka, Docker, …). Typing an alias or product name finds them too, so `kafka` offers `apache-kafka`. An unknown brand is reported on its own line.
|
||||
- **Schema help**: completion and hover from Archify's own schemas, through the YAML extension for `.archify` files.
|
||||
- **Source links**: in source-backed diagrams, clicking a source reference opens that file and line range in the editor. If the file is not in the workspace, the web link opens instead.
|
||||
- **Export**: the diagram's own Export menu (PNG, SVG, share card, …) saves through a VS Code save dialog.
|
||||
- **Commands**: *Archify: Render to HTML File…*, *Archify: Open in Browser*, *Archify: Refresh Preview*, *Archify: Show Source*.
|
||||
- **Commands**: *Archify | New Diagram…*, *Archify | Render to HTML File…*, *Archify | Open in Browser*, *Archify | Refresh Preview*, *Archify | Show Source*.
|
||||
|
||||
## Source evidence
|
||||
|
||||
A diagram that declares `meta.repository` and `sources` is verified against a local Git checkout whose `origin` and pinned commit match. The extension uses the Git top-level directory of the diagram file, or `archify.repoRoot` when set.
|
||||
|
||||
When verification fails, the default (`archify.sourceEvidence: "fallback"`) still shows the diagram without source links, explains why in a notice, and reports the reason as a warning. Set it to `"strict"` to treat this as an error, as the Archify CLI does. *Render to HTML File…* always verifies strictly and asks before rendering without source links.
|
||||
When verification fails, the default (`archify.sourceEvidence: "fallback"`) still shows the diagram without source links, explains why in a notice, and reports the reason as a warning. Set it to `"strict"` to treat this as an error, as the Archify CLI does. *Archify | Render to HTML File…* always verifies strictly and asks before rendering without source links.
|
||||
|
||||
## Settings
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "archify-vscode-ext",
|
||||
"version": "0.2.0",
|
||||
"version": "0.5.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "archify-vscode-ext",
|
||||
"version": "0.2.0",
|
||||
"version": "0.5.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"yaml": "^2.9.1"
|
||||
|
||||
+31
-21
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "archify-vscode-ext",
|
||||
"displayName": "Archify Diagram Viewer",
|
||||
"description": "Visualize Archify diagrams: .archify files (YAML) open straight into the interactive diagram; also previews Archify JSON sources and rendered Archify HTML inside VS Code.",
|
||||
"version": "0.3.0",
|
||||
"description": "Archify diagrams in VS Code. Write diagrams as .archify files in YAML: they open as the interactive diagram, with live updates, validation, templates and completion. Also previews Archify's own JSON files (*.architecture.json, \u2026) and opens rendered Archify HTML.",
|
||||
"version": "0.5.2",
|
||||
"publisher": "root-at-skic",
|
||||
"license": "MIT",
|
||||
"icon": "media/icon.png",
|
||||
@@ -23,6 +23,7 @@
|
||||
],
|
||||
"keywords": [
|
||||
"archify",
|
||||
"yaml",
|
||||
"diagram",
|
||||
"architecture",
|
||||
"sequence",
|
||||
@@ -38,46 +39,44 @@
|
||||
],
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "archify.newDiagram",
|
||||
"title": "Archify | New Diagram\u2026",
|
||||
"icon": "$(new-file)"
|
||||
},
|
||||
{
|
||||
"command": "archify.showPreview",
|
||||
"title": "Open Preview",
|
||||
"category": "Archify",
|
||||
"title": "Archify | Open Preview",
|
||||
"icon": "$(open-preview)"
|
||||
},
|
||||
{
|
||||
"command": "archify.showPreviewToSide",
|
||||
"title": "Open Preview to the Side",
|
||||
"category": "Archify",
|
||||
"title": "Archify | Open Preview to the Side",
|
||||
"icon": "$(open-preview)"
|
||||
},
|
||||
{
|
||||
"command": "archify.openHtmlViewer",
|
||||
"title": "Open in Archify Viewer",
|
||||
"category": "Archify",
|
||||
"title": "Archify | Open in HTML Viewer",
|
||||
"icon": "$(open-preview)"
|
||||
},
|
||||
{
|
||||
"command": "archify.renderToHtml",
|
||||
"title": "Render to HTML File\u2026",
|
||||
"category": "Archify",
|
||||
"title": "Archify | Render to HTML File\u2026",
|
||||
"icon": "$(file-code)"
|
||||
},
|
||||
{
|
||||
"command": "archify.openInBrowser",
|
||||
"title": "Open in Browser",
|
||||
"category": "Archify",
|
||||
"title": "Archify | Open in Browser",
|
||||
"icon": "$(globe)"
|
||||
},
|
||||
{
|
||||
"command": "archify.refreshPreview",
|
||||
"title": "Refresh Preview",
|
||||
"category": "Archify",
|
||||
"title": "Archify | Refresh Preview",
|
||||
"icon": "$(refresh)"
|
||||
},
|
||||
{
|
||||
"command": "archify.showSource",
|
||||
"title": "Show Source",
|
||||
"category": "Archify",
|
||||
"title": "Archify | Show Source",
|
||||
"icon": "$(go-to-file)"
|
||||
}
|
||||
],
|
||||
@@ -145,27 +144,38 @@
|
||||
}
|
||||
],
|
||||
"explorer/context": [
|
||||
{
|
||||
"command": "archify.newDiagram",
|
||||
"when": "explorerResourceIsFolder",
|
||||
"group": "zzz_archify@1"
|
||||
},
|
||||
{
|
||||
"command": "archify.showPreview",
|
||||
"when": "resourceFilename =~ /(\\.archify|\\.(architecture|workflow|sequence|dataflow|lifecycle)\\.json)$/",
|
||||
"group": "navigation@20"
|
||||
"group": "zzz_archify@2"
|
||||
},
|
||||
{
|
||||
"command": "archify.renderToHtml",
|
||||
"when": "resourceFilename =~ /(\\.archify|\\.(architecture|workflow|sequence|dataflow|lifecycle)\\.json)$/",
|
||||
"group": "navigation@21"
|
||||
"group": "zzz_archify@3"
|
||||
},
|
||||
{
|
||||
"command": "archify.openHtmlViewer",
|
||||
"when": "resourceExtname == .html",
|
||||
"group": "navigation@20"
|
||||
"group": "zzz_archify@2"
|
||||
}
|
||||
],
|
||||
"editor/title/context": [
|
||||
{
|
||||
"command": "archify.showPreview",
|
||||
"when": "resourceFilename =~ /(\\.archify|\\.(architecture|workflow|sequence|dataflow|lifecycle)\\.json)$/",
|
||||
"group": "1_open"
|
||||
"group": "zzz_archify@1"
|
||||
}
|
||||
],
|
||||
"file/newFile": [
|
||||
{
|
||||
"command": "archify.newDiagram",
|
||||
"group": "file"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -196,7 +206,7 @@
|
||||
},
|
||||
{
|
||||
"viewType": "archify.htmlViewer",
|
||||
"displayName": "Archify Diagram Viewer",
|
||||
"displayName": "Archify HTML Viewer",
|
||||
"selector": [
|
||||
{
|
||||
"filenamePattern": "*.html"
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import path from 'node:path';
|
||||
import * as vscode from 'vscode';
|
||||
import { Brand, brandValueAt, loadBrandCatalog } from './brands';
|
||||
import { isArchifyFile } from './detect';
|
||||
import { log } from './runtime';
|
||||
|
||||
const JSON_SOURCES = '**/*.{architecture,workflow,sequence,dataflow,lifecycle}.json';
|
||||
|
||||
export const BRAND_COMPLETION_SELECTOR: vscode.DocumentSelector = [
|
||||
{ pattern: '**/*.archify' },
|
||||
{ language: 'json', pattern: JSON_SOURCES },
|
||||
{ language: 'jsonc', pattern: JSON_SOURCES },
|
||||
];
|
||||
|
||||
/** Suggests Archify's built-in brand IDs for `brand` values. */
|
||||
export class BrandCompletionProvider implements vscode.CompletionItemProvider {
|
||||
private brands: Brand[] | undefined;
|
||||
|
||||
constructor(private readonly extensionPath: string) {}
|
||||
|
||||
private catalog(): Brand[] {
|
||||
if (!this.brands) {
|
||||
try {
|
||||
this.brands = loadBrandCatalog(path.join(this.extensionPath, 'vendor', 'archify', 'brand-marks', 'catalog.json'));
|
||||
} catch (error) {
|
||||
log().error(`Could not read the Archify brand catalog: ${error instanceof Error ? error.message : String(error)}`);
|
||||
this.brands = [];
|
||||
}
|
||||
}
|
||||
return this.brands;
|
||||
}
|
||||
|
||||
provideCompletionItems(document: vscode.TextDocument, position: vscode.Position): vscode.CompletionItem[] | undefined {
|
||||
const linePrefix = document.lineAt(position.line).text.slice(0, position.character);
|
||||
const value = brandValueAt(linePrefix, isArchifyFile(document.fileName) ? 'yaml' : 'json');
|
||||
if (!value) return undefined;
|
||||
const range = new vscode.Range(position.translate(0, -value.typed.length), position);
|
||||
return this.catalog().map((brand) => {
|
||||
const item = new vscode.CompletionItem({ label: brand.id, description: brand.title }, vscode.CompletionItemKind.Value);
|
||||
item.detail = `${brand.title} · ${brand.category}`;
|
||||
if (brand.aliases.length) item.documentation = `Also matches: ${brand.aliases.join(', ')}`;
|
||||
// Typing an alias or the product name finds the brand too.
|
||||
item.filterText = [brand.id, ...brand.aliases, brand.title].join(' ');
|
||||
item.range = range;
|
||||
return item;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import fs from 'node:fs';
|
||||
|
||||
export interface Brand {
|
||||
id: string;
|
||||
title: string;
|
||||
category: string;
|
||||
aliases: string[];
|
||||
}
|
||||
|
||||
/** Archify's built-in brand marks, read from brand-marks/catalog.json. */
|
||||
export function loadBrandCatalog(file: string): Brand[] {
|
||||
const catalog = JSON.parse(fs.readFileSync(file, 'utf8')) as { marks?: Partial<Brand>[] };
|
||||
return (catalog.marks ?? [])
|
||||
.filter((mark): mark is Partial<Brand> & { id: string } => typeof mark.id === 'string')
|
||||
.map((mark) => ({ id: mark.id, title: mark.title ?? mark.id, category: mark.category ?? '', aliases: mark.aliases ?? [] }));
|
||||
}
|
||||
|
||||
export interface BrandValue {
|
||||
/** What has been typed of the value so far. */
|
||||
typed: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the text before the cursor ends inside a `brand` value:
|
||||
* `brand: post` or `{ id: db, brand: "post` in YAML, `"brand": "post` in JSON.
|
||||
*/
|
||||
export function brandValueAt(linePrefix: string, format: 'yaml' | 'json'): BrandValue | undefined {
|
||||
const match =
|
||||
format === 'yaml'
|
||||
? /(?:^|[\s{,])brand:\s*["']?([\w.-]*)$/.exec(linePrefix)
|
||||
: /"brand"\s*:\s*"([\w.-]*)$/.exec(linePrefix);
|
||||
return match ? { typed: match[1] } : undefined;
|
||||
}
|
||||
+3
-2
@@ -1,7 +1,7 @@
|
||||
import { parse, ParseError } from 'jsonc-parser';
|
||||
import * as vscode from 'vscode';
|
||||
import { isArchifyFile, readDiagramSource } from './detect';
|
||||
import { rangeForPointer, rangeForPointerYaml, yamlSyntaxProblems } from './jsonPointer';
|
||||
import { diagnosticPointer, rangeForPointer, rangeForPointerYaml, yamlSyntaxProblems } from './jsonPointer';
|
||||
import { ArchifyDiagnostic, isEvidenceError, validate } from './renderer';
|
||||
import { evidenceMode, log, runtimeFor } from './runtime';
|
||||
|
||||
@@ -133,7 +133,8 @@ export class DiagnosticsController implements vscode.Disposable {
|
||||
this.collection.set(
|
||||
doc.uri,
|
||||
result.diagnostics.map((item) => {
|
||||
const offsets = source.format === 'yaml' ? rangeForPointerYaml(text, item.subject?.path) : rangeForPointer(text, item.subject?.path);
|
||||
const pointer = diagnosticPointer(item);
|
||||
const offsets = source.format === 'yaml' ? rangeForPointerYaml(text, pointer) : rangeForPointer(text, pointer);
|
||||
const range = new vscode.Range(doc.positionAt(offsets.start), doc.positionAt(offsets.end));
|
||||
const fix = item.supportedFixes?.[0];
|
||||
const diagnostic = new vscode.Diagnostic(
|
||||
|
||||
+68
-2
@@ -1,12 +1,14 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import * as vscode from 'vscode';
|
||||
import { BRAND_COMPLETION_SELECTOR, BrandCompletionProvider } from './brandCompletion';
|
||||
import { diagramBaseName, isArchifyFile, isArchifyHtml, isDiagramFileName, readDiagramSource } from './detect';
|
||||
import { DiagnosticsController } from './diagnostics';
|
||||
import { HTML_VIEW_TYPE, HtmlViewerProvider } from './htmlViewer';
|
||||
import { DIAGRAM_EDITOR_VIEW_TYPE, PreviewManager } from './preview';
|
||||
import { isEvidenceError, renderDiagram, renderToFile } from './renderer';
|
||||
import { clearGitRootCache, evidenceMode, log, readText, runtimeFor } from './runtime';
|
||||
import { diagramFromTemplate, diagramName, invalidNameReason, TEMPLATES } from './templates';
|
||||
|
||||
const JSON_LANGUAGES = new Set(['json', 'jsonc']);
|
||||
|
||||
@@ -17,7 +19,13 @@ function isHtmlUri(uri: vscode.Uri): boolean {
|
||||
export function activate(context: vscode.ExtensionContext): void {
|
||||
const previews = new PreviewManager(context);
|
||||
const htmlViewer = new HtmlViewerProvider();
|
||||
context.subscriptions.push(previews, htmlViewer, new DiagnosticsController(context), log());
|
||||
context.subscriptions.push(
|
||||
previews,
|
||||
htmlViewer,
|
||||
new DiagnosticsController(context),
|
||||
log(),
|
||||
vscode.languages.registerCompletionItemProvider(BRAND_COMPLETION_SELECTOR, new BrandCompletionProvider(context.extensionPath), ' ', '"'),
|
||||
);
|
||||
|
||||
/** The file a command applies to: explicit argument, focused preview or viewer, then the text editor. */
|
||||
function targetUri(arg?: unknown): vscode.Uri | undefined {
|
||||
@@ -27,7 +35,7 @@ export function activate(context: vscode.ExtensionContext): void {
|
||||
|
||||
function sourceUri(arg?: unknown): vscode.Uri | undefined {
|
||||
const uri = targetUri(arg);
|
||||
if (!uri) vscode.window.showWarningMessage('Archify: open a diagram JSON source first.');
|
||||
if (!uri) vscode.window.showWarningMessage('Archify: open a .archify diagram first.');
|
||||
return uri;
|
||||
}
|
||||
|
||||
@@ -61,6 +69,26 @@ export function activate(context: vscode.ExtensionContext): void {
|
||||
const openHtmlViewer = (uri: vscode.Uri, viewColumn?: vscode.ViewColumn) =>
|
||||
vscode.commands.executeCommand('vscode.openWith', uri, HTML_VIEW_TYPE, viewColumn);
|
||||
|
||||
const exists = async (uri: vscode.Uri) => {
|
||||
try {
|
||||
await vscode.workspace.fs.stat(uri);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/** Folder for a new diagram: the clicked folder (or a file's folder), else a workspace folder. */
|
||||
async function newDiagramFolder(arg?: unknown): Promise<vscode.Uri | undefined> {
|
||||
if (arg instanceof vscode.Uri) {
|
||||
const stat = await vscode.workspace.fs.stat(arg);
|
||||
return stat.type & vscode.FileType.Directory ? arg : vscode.Uri.joinPath(arg, '..');
|
||||
}
|
||||
const folders = vscode.workspace.workspaceFolders ?? [];
|
||||
if (folders.length <= 1) return folders[0]?.uri;
|
||||
return (await vscode.window.showWorkspaceFolderPick({ placeHolder: 'Folder for the new diagram' }))?.uri;
|
||||
}
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('archify.showPreview', (arg?: unknown) => {
|
||||
const uri = sourceUri(arg);
|
||||
@@ -88,6 +116,44 @@ export function activate(context: vscode.ExtensionContext): void {
|
||||
if (uri) return openHtmlViewer(uri);
|
||||
}),
|
||||
|
||||
vscode.commands.registerCommand('archify.newDiagram', async (arg?: unknown) => {
|
||||
const template = await vscode.window.showQuickPick(
|
||||
TEMPLATES.map((item) => ({ label: item.label, description: item.description, template: item })),
|
||||
{ title: 'Archify | New Diagram', placeHolder: 'Kind of diagram' },
|
||||
);
|
||||
if (!template) return;
|
||||
const { file, defaultName } = template.template;
|
||||
|
||||
const folder = await newDiagramFolder(arg);
|
||||
let target: vscode.Uri | undefined;
|
||||
if (folder) {
|
||||
const name = await vscode.window.showInputBox({
|
||||
title: 'Archify | New Diagram',
|
||||
prompt: `File name in ${vscode.workspace.asRelativePath(folder, true)}`,
|
||||
value: defaultName,
|
||||
validateInput: async (value) =>
|
||||
invalidNameReason(value) ??
|
||||
((await exists(vscode.Uri.joinPath(folder, `${diagramName(value)}.archify`))) ? `${diagramName(value)}.archify already exists.` : undefined),
|
||||
});
|
||||
if (name === undefined) return;
|
||||
target = vscode.Uri.joinPath(folder, `${diagramName(name)}.archify`);
|
||||
} else {
|
||||
// No workspace: let the user choose where to save.
|
||||
target = await vscode.window.showSaveDialog({
|
||||
title: 'Archify | New Diagram',
|
||||
filters: { 'Archify Diagram': ['archify'] },
|
||||
saveLabel: 'Create',
|
||||
});
|
||||
if (!target) return;
|
||||
if (!isArchifyFile(target.path)) target = target.with({ path: `${target.path}.archify` });
|
||||
}
|
||||
|
||||
const templateText = await readText(vscode.Uri.joinPath(context.extensionUri, 'templates', file));
|
||||
const text = diagramFromTemplate(templateText, diagramBaseName(target.path));
|
||||
await vscode.workspace.fs.writeFile(target, Buffer.from(text, 'utf8'));
|
||||
await vscode.commands.executeCommand('vscode.openWith', target, DIAGRAM_EDITOR_VIEW_TYPE);
|
||||
}),
|
||||
|
||||
vscode.commands.registerCommand('archify.refreshPreview', () => previews.activePreview?.update(true)),
|
||||
|
||||
vscode.commands.registerCommand('archify.showSource', async () => {
|
||||
|
||||
@@ -6,6 +6,14 @@ export interface OffsetRange {
|
||||
end: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The JSON pointer an Archify diagnostic is about. Most diagnostics carry it
|
||||
* in subject.path; brand diagnostics only start their message with it.
|
||||
*/
|
||||
export function diagnosticPointer(diagnostic: { message: string; subject?: { path?: string } }): string | undefined {
|
||||
return diagnostic.subject?.path ?? /^(\/[^\s"]+)\s/.exec(diagnostic.message)?.[1];
|
||||
}
|
||||
|
||||
/** Splits an RFC 6901 JSON pointer into path segments. */
|
||||
export function pointerSegments(pointer: string): (string | number)[] {
|
||||
if (!pointer || pointer === '/') return [];
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import path from 'node:path';
|
||||
import * as vscode from 'vscode';
|
||||
import { readDiagramSource } from './detect';
|
||||
import { isArchifyFile, readDiagramSource } from './detect';
|
||||
import { renderDiagram } from './renderer';
|
||||
import { evidenceMode, log, readText, runtimeFor, viewerTheme } from './runtime';
|
||||
import { handleWebviewMessage, WebviewMessage } from './webviewMessages';
|
||||
@@ -100,7 +100,7 @@ class DiagramPreview implements vscode.Disposable {
|
||||
if (!source) {
|
||||
this.showMessage(
|
||||
'Nothing to show yet',
|
||||
'Set "diagram_type" to architecture, workflow, sequence, dataflow or lifecycle, and make sure the file is valid JSON.',
|
||||
`Set "diagram_type" to architecture, workflow, sequence, dataflow or lifecycle, and make sure the file is valid ${isArchifyFile(this.uri.path) ? 'YAML' : 'JSON'}.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { isMap, parseDocument } from 'yaml';
|
||||
import { DiagramType } from './detect';
|
||||
|
||||
export interface DiagramTemplate {
|
||||
/** File name under templates/. */
|
||||
file: string;
|
||||
type: DiagramType;
|
||||
label: string;
|
||||
description: string;
|
||||
/** Suggested name for the new file, without .archify. */
|
||||
defaultName: string;
|
||||
}
|
||||
|
||||
export const TEMPLATES: readonly DiagramTemplate[] = [
|
||||
{ file: 'architecture.archify', type: 'architecture', label: 'Architecture', description: 'Parts of a system and how they connect', defaultName: 'architecture' },
|
||||
{ file: 'workflow.archify', type: 'workflow', label: 'Workflow', description: 'Steps of a process, in lanes', defaultName: 'workflow' },
|
||||
{ file: 'sequence.archify', type: 'sequence', label: 'Sequence', description: 'Who calls whom, in order', defaultName: 'sequence' },
|
||||
{ file: 'dataflow.archify', type: 'dataflow', label: 'Data Flow', description: 'Where data comes from and where it goes', defaultName: 'dataflow' },
|
||||
{ file: 'lifecycle.archify', type: 'lifecycle', label: 'Lifecycle', description: 'States something moves through', defaultName: 'lifecycle' },
|
||||
{ file: 'everyday-plan.archify', type: 'workflow', label: 'Everyday Plan', description: 'A plan or process from daily life', defaultName: 'plan' },
|
||||
];
|
||||
|
||||
/** Strips a typed .archify suffix and surrounding spaces. */
|
||||
export function diagramName(input: string): string {
|
||||
return input.trim().replace(/\.archify$/i, '').trim();
|
||||
}
|
||||
|
||||
/** Why a name cannot be used as a file name, or undefined when it can. */
|
||||
export function invalidNameReason(input: string): string | undefined {
|
||||
const name = diagramName(input);
|
||||
if (!name) return 'Enter a name.';
|
||||
if (/[\\/:*?"<>|\u0000-\u001f]/.test(name)) return 'A name cannot contain \\ / : * ? " < > | or control characters.';
|
||||
if (/^\.+$/.test(name)) return 'Enter a name.';
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** "sales-report_v2" → "Sales report v2". */
|
||||
export function titleFromName(name: string): string {
|
||||
const words = name.replace(/[-_.]+/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
return words ? words.charAt(0).toUpperCase() + words.slice(1) : name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template YAML with meta.title and meta.output set for the new file.
|
||||
* Works on the YAML document so the template's comments and layout stay.
|
||||
*/
|
||||
export function diagramFromTemplate(templateText: string, name: string): string {
|
||||
const doc = parseDocument(templateText);
|
||||
if (!isMap(doc.get('meta'))) doc.set('meta', doc.createNode({}));
|
||||
doc.setIn(['meta', 'title'], titleFromName(name));
|
||||
doc.setIn(['meta', 'output'], `${name}.html`);
|
||||
return doc.toString({ lineWidth: 0, flowCollectionPadding: false });
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
# Architecture: the parts of a system and how they connect.
|
||||
# Components need a unique id; connections link component ids.
|
||||
schema_version: 1
|
||||
diagram_type: architecture
|
||||
meta:
|
||||
title: Web Application
|
||||
output: architecture.html
|
||||
components:
|
||||
- id: users
|
||||
type: external
|
||||
label: Users
|
||||
sublabel: Browser
|
||||
pos: [40, 160]
|
||||
size: [130, 60]
|
||||
- id: api
|
||||
type: backend
|
||||
label: API
|
||||
sublabel: REST service
|
||||
pos: [250, 160]
|
||||
size: [130, 60]
|
||||
- id: cache
|
||||
type: database
|
||||
label: Cache
|
||||
sublabel: Redis
|
||||
pos: [460, 60]
|
||||
size: [130, 60]
|
||||
- id: db
|
||||
type: database
|
||||
label: Database
|
||||
sublabel: PostgreSQL
|
||||
pos: [460, 260]
|
||||
size: [130, 60]
|
||||
boundaries:
|
||||
- kind: region
|
||||
label: Production
|
||||
wraps: [api, cache, db]
|
||||
connections:
|
||||
- id: users-api
|
||||
from: users
|
||||
to: api
|
||||
label: HTTPS
|
||||
variant: emphasis
|
||||
- id: api-cache
|
||||
from: api
|
||||
to: cache
|
||||
label: read
|
||||
- id: api-db
|
||||
from: api
|
||||
to: db
|
||||
label: SQL
|
||||
@@ -0,0 +1,55 @@
|
||||
# Data flow: where data comes from, how it is transformed, and where it ends up.
|
||||
# Nodes sit in stages (columns) and rows; flows link node ids.
|
||||
schema_version: 1
|
||||
diagram_type: dataflow
|
||||
meta:
|
||||
title: Sales Reporting
|
||||
output: dataflow.html
|
||||
viewBox: [960, 420]
|
||||
stages:
|
||||
- label: Sources
|
||||
- label: Processing
|
||||
- label: Storage
|
||||
- label: Consumers
|
||||
nodes:
|
||||
- id: shop
|
||||
type: frontend
|
||||
label: Web Shop
|
||||
stage: 0
|
||||
row: 0
|
||||
- id: crm
|
||||
type: external
|
||||
label: CRM
|
||||
stage: 0
|
||||
row: 1
|
||||
- id: etl
|
||||
type: backend
|
||||
label: Nightly ETL
|
||||
stage: 1
|
||||
row: 0
|
||||
- id: warehouse
|
||||
type: database
|
||||
label: Warehouse
|
||||
stage: 2
|
||||
row: 0
|
||||
- id: dashboard
|
||||
type: frontend
|
||||
label: Sales Dashboard
|
||||
stage: 3
|
||||
row: 0
|
||||
flows:
|
||||
- from: shop
|
||||
to: etl
|
||||
label: orders
|
||||
- from: crm
|
||||
to: etl
|
||||
label: customers
|
||||
classification: PII
|
||||
variant: security
|
||||
- from: etl
|
||||
to: warehouse
|
||||
label: daily facts
|
||||
variant: emphasis
|
||||
- from: warehouse
|
||||
to: dashboard
|
||||
label: aggregates
|
||||
@@ -0,0 +1,72 @@
|
||||
# Everyday plan: a workflow about daily life rather than software.
|
||||
# Node types set the colour; meta.legend renames them for the reader,
|
||||
# and icon picks the corner symbol (calendar, clock, person, briefcase, flag, moon).
|
||||
schema_version: 2
|
||||
diagram_type: workflow
|
||||
meta:
|
||||
title: Summer Holiday Plan
|
||||
output: everyday-plan.html
|
||||
legend:
|
||||
entries:
|
||||
backend:
|
||||
label: Time off
|
||||
security:
|
||||
label: Work
|
||||
external:
|
||||
label: Milestone
|
||||
lanes:
|
||||
- id: plan
|
||||
label: Plan
|
||||
- id: work
|
||||
label: Work
|
||||
mainPath: [today, book, holiday, back]
|
||||
nodes:
|
||||
- id: today
|
||||
lane: plan
|
||||
col: 0
|
||||
type: external
|
||||
label: Today
|
||||
icon: clock
|
||||
- id: book
|
||||
lane: plan
|
||||
col: 1
|
||||
type: external
|
||||
label: Book flights
|
||||
sublabel: by end of May
|
||||
icon: flag
|
||||
- id: holiday
|
||||
lane: plan
|
||||
col: 2
|
||||
type: backend
|
||||
label: Holiday
|
||||
sublabel: 2 weeks
|
||||
icon: calendar
|
||||
- id: back
|
||||
lane: plan
|
||||
col: 3
|
||||
type: external
|
||||
label: Back home
|
||||
icon: flag
|
||||
- id: handover
|
||||
lane: work
|
||||
col: 1
|
||||
type: security
|
||||
label: Handover
|
||||
icon: briefcase
|
||||
edges:
|
||||
- id: plan-booking
|
||||
from: today
|
||||
to: book
|
||||
- id: go
|
||||
from: book
|
||||
to: holiday
|
||||
label: leave approved
|
||||
- id: return
|
||||
from: holiday
|
||||
to: back
|
||||
- id: prepare
|
||||
from: today
|
||||
to: handover
|
||||
label: before leaving
|
||||
role: branch
|
||||
variant: security
|
||||
@@ -0,0 +1,53 @@
|
||||
# Lifecycle: the states something moves through and what moves it.
|
||||
# States sit in lanes and columns; transitions link state ids.
|
||||
schema_version: 1
|
||||
diagram_type: lifecycle
|
||||
meta:
|
||||
title: Order Status
|
||||
output: lifecycle.html
|
||||
viewBox: [880, 566]
|
||||
lanes:
|
||||
- id: main
|
||||
label: Normal path
|
||||
- id: exits
|
||||
label: Exits
|
||||
states:
|
||||
- id: placed
|
||||
type: start
|
||||
label: Placed
|
||||
lane: main
|
||||
col: 0
|
||||
- id: paid
|
||||
type: active
|
||||
label: Paid
|
||||
lane: main
|
||||
col: 1
|
||||
- id: shipped
|
||||
type: active
|
||||
label: Shipped
|
||||
lane: main
|
||||
col: 2
|
||||
- id: delivered
|
||||
type: success
|
||||
label: Delivered
|
||||
lane: main
|
||||
col: 3
|
||||
- id: cancelled
|
||||
type: failure
|
||||
label: Cancelled
|
||||
lane: exits
|
||||
col: 0
|
||||
transitions:
|
||||
- id: pay
|
||||
from: placed
|
||||
to: paid
|
||||
- id: ship
|
||||
from: paid
|
||||
to: shipped
|
||||
- id: deliver
|
||||
from: shipped
|
||||
to: delivered
|
||||
- id: cancel
|
||||
from: placed
|
||||
to: cancelled
|
||||
variant: security
|
||||
@@ -0,0 +1,50 @@
|
||||
# Sequence: who calls whom, in order. Participants are columns;
|
||||
# messages run top to bottom by y.
|
||||
schema_version: 1
|
||||
diagram_type: sequence
|
||||
meta:
|
||||
title: Login Request
|
||||
output: sequence.html
|
||||
viewBox: [960, 520]
|
||||
participants:
|
||||
- id: client
|
||||
type: external
|
||||
label: Client
|
||||
- id: api
|
||||
type: backend
|
||||
label: API
|
||||
- id: auth
|
||||
type: security
|
||||
label: Auth Service
|
||||
- id: db
|
||||
type: database
|
||||
label: User Store
|
||||
messages:
|
||||
- from: client
|
||||
to: api
|
||||
y: 160
|
||||
label: POST /login
|
||||
variant: emphasis
|
||||
- from: api
|
||||
to: auth
|
||||
y: 210
|
||||
label: verify credentials
|
||||
- from: auth
|
||||
to: db
|
||||
y: 260
|
||||
label: find user
|
||||
- from: db
|
||||
to: auth
|
||||
y: 310
|
||||
label: user record
|
||||
variant: return
|
||||
- from: auth
|
||||
to: api
|
||||
y: 360
|
||||
label: token
|
||||
variant: return
|
||||
- from: api
|
||||
to: client
|
||||
y: 410
|
||||
label: 200 OK + token
|
||||
variant: return
|
||||
@@ -0,0 +1,70 @@
|
||||
# Workflow: steps of a process, grouped in lanes, left to right by col.
|
||||
# mainPath lists the happy path; edges link node ids.
|
||||
schema_version: 2
|
||||
diagram_type: workflow
|
||||
meta:
|
||||
title: Change Approval
|
||||
output: workflow.html
|
||||
# Rename the legend entries of the node types you use.
|
||||
legend:
|
||||
entries:
|
||||
frontend:
|
||||
label: Author step
|
||||
security:
|
||||
label: Review gate
|
||||
backend:
|
||||
label: Decision
|
||||
external:
|
||||
label: Outcome
|
||||
lanes:
|
||||
- id: author
|
||||
label: Author
|
||||
- id: review
|
||||
label: Reviewers
|
||||
mainPath: [draft, review, approved, released]
|
||||
nodes:
|
||||
- id: draft
|
||||
lane: author
|
||||
col: 0
|
||||
type: frontend
|
||||
label: Draft change
|
||||
- id: review
|
||||
lane: review
|
||||
col: 1
|
||||
type: security
|
||||
label: Review
|
||||
sublabel: two approvals
|
||||
- id: approved
|
||||
lane: review
|
||||
col: 2
|
||||
type: backend
|
||||
label: Approved
|
||||
- id: released
|
||||
lane: author
|
||||
col: 3
|
||||
type: external
|
||||
label: Released
|
||||
- id: rework
|
||||
lane: author
|
||||
col: 1
|
||||
type: frontend
|
||||
label: Rework
|
||||
edges:
|
||||
- id: submit
|
||||
from: draft
|
||||
to: review
|
||||
label: submit
|
||||
- id: approve
|
||||
from: review
|
||||
to: approved
|
||||
label: approve
|
||||
- id: release
|
||||
from: approved
|
||||
to: released
|
||||
label: release
|
||||
- id: request-changes
|
||||
from: review
|
||||
to: rework
|
||||
label: changes requested
|
||||
role: branch
|
||||
variant: dashed
|
||||
@@ -0,0 +1,44 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import path from 'node:path';
|
||||
import { test } from 'node:test';
|
||||
import { brandValueAt, loadBrandCatalog } from '../src/brands';
|
||||
import { diagnosticPointer } from '../src/jsonPointer';
|
||||
|
||||
const root = path.resolve(__dirname, '..', '..');
|
||||
const catalogFile = path.join(root, 'vendor', 'archify', 'brand-marks', 'catalog.json');
|
||||
|
||||
test('loads the bundled brand catalog', () => {
|
||||
const brands = loadBrandCatalog(catalogFile);
|
||||
assert.ok(brands.length >= 100, `${brands.length} brands`);
|
||||
const postgres = brands.find((brand) => brand.id === 'postgresql');
|
||||
assert.ok(postgres);
|
||||
assert.ok(postgres.aliases.includes('postgres'));
|
||||
assert.ok(brands.every((brand) => brand.id && brand.title));
|
||||
});
|
||||
|
||||
test('finds a brand value being typed in YAML', () => {
|
||||
assert.deepEqual(brandValueAt(' brand: ', 'yaml'), { typed: '' });
|
||||
assert.deepEqual(brandValueAt(' brand: post', 'yaml'), { typed: 'post' });
|
||||
assert.deepEqual(brandValueAt(' - brand: apache-ka', 'yaml'), { typed: 'apache-ka' });
|
||||
assert.deepEqual(brandValueAt(' brand: "red', 'yaml'), { typed: 'red' });
|
||||
assert.deepEqual(brandValueAt(' - { id: db, brand: post', 'yaml'), { typed: 'post' });
|
||||
assert.equal(brandValueAt(' label: post', 'yaml'), undefined);
|
||||
assert.equal(brandValueAt(' subbrand: post', 'yaml'), undefined);
|
||||
assert.equal(brandValueAt(' brand: postgresql ', 'yaml'), undefined, 'value already finished');
|
||||
});
|
||||
|
||||
test('finds a brand value being typed in JSON', () => {
|
||||
assert.deepEqual(brandValueAt(' "brand": "', 'json'), { typed: '' });
|
||||
assert.deepEqual(brandValueAt('{ "id": "db", "brand": "post', 'json'), { typed: 'post' });
|
||||
assert.equal(brandValueAt(' "brand": ', 'json'), undefined, 'not inside the string yet');
|
||||
assert.equal(brandValueAt(' "label": "post', 'json'), undefined);
|
||||
});
|
||||
|
||||
test('diagnostic pointer comes from subject.path, else from a leading pointer in the message', () => {
|
||||
assert.equal(diagnosticPointer({ message: 'x', subject: { path: '/components/0' } }), '/components/0');
|
||||
assert.equal(
|
||||
diagnosticPointer({ message: '/components/0/brand "aws" is not a built-in brand; closest IDs: airtable' }),
|
||||
'/components/0/brand',
|
||||
);
|
||||
assert.equal(diagnosticPointer({ message: 'Label "x" is wider than node' }), undefined);
|
||||
});
|
||||
@@ -97,6 +97,88 @@ 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('Archify | New Diagram creates a .archify file from a template and opens the diagram', async () => {
|
||||
// Menus show the bare title, so it must name Archify by itself.
|
||||
const commands = vscode.extensions.getExtension(EXTENSION_ID)?.packageJSON.contributes.commands as { command: string; title: string; category?: string }[];
|
||||
const newDiagram = commands.find((item) => item.command === 'archify.newDiagram');
|
||||
assert.equal(newDiagram?.title, 'Archify | New Diagram…');
|
||||
assert.equal(newDiagram?.category, undefined);
|
||||
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('suggests brand IDs after brand: in .archify files', async () => {
|
||||
const target = workspaceFile('brands.archify');
|
||||
const text = [
|
||||
'schema_version: 1',
|
||||
'diagram_type: architecture',
|
||||
'meta:',
|
||||
' title: Brands',
|
||||
'components:',
|
||||
' - id: db',
|
||||
' type: database',
|
||||
' label: DB',
|
||||
' brand: ',
|
||||
' - id: bus',
|
||||
' type: messagebus',
|
||||
' label: Bus',
|
||||
' brand: kafka',
|
||||
'',
|
||||
].join('\n');
|
||||
await vscode.workspace.fs.writeFile(target, new TextEncoder().encode(text));
|
||||
const doc = await vscode.workspace.openTextDocument(target);
|
||||
const labels = async (line: number) => {
|
||||
const list = await vscode.commands.executeCommand<vscode.CompletionList>(
|
||||
'vscode.executeCompletionItemProvider',
|
||||
doc.uri,
|
||||
new vscode.Position(line, doc.lineAt(line).text.length),
|
||||
);
|
||||
return list.items.map((item) => (typeof item.label === 'string' ? item.label : item.label.label));
|
||||
};
|
||||
const empty = await labels(8);
|
||||
assert.ok(empty.includes('postgresql') && empty.includes('redis'), 'brand IDs offered after "brand: "');
|
||||
const kafka = await labels(12);
|
||||
assert.ok(kafka.includes('apache-kafka'), 'the alias "kafka" finds apache-kafka');
|
||||
assert.equal((await labels(7)).includes('postgresql'), false, 'nothing offered for other fields');
|
||||
});
|
||||
|
||||
await check('an unknown brand is reported on its own line', async () => {
|
||||
const target = workspaceFile('unknown-brand.archify');
|
||||
const text = new TextDecoder().decode(await vscode.workspace.fs.readFile(workspaceFile('web-platform.archify')))
|
||||
.replace(/^( {2}- id: [^\n]+\n)/m, '$1 brand: not-a-brand\n');
|
||||
await vscode.workspace.fs.writeFile(target, new TextEncoder().encode(text));
|
||||
const doc = await vscode.workspace.openTextDocument(target);
|
||||
await vscode.window.showTextDocument(doc);
|
||||
const diagnostic = await waitFor('brand/unknown diagnostic', () =>
|
||||
vscode.languages.getDiagnostics(doc.uri).find((d) => d.source === 'archify' && d.code === 'brand/unknown'),
|
||||
);
|
||||
assert.match(doc.lineAt(diagnostic.range.start.line).text, /brand: not-a-brand/);
|
||||
});
|
||||
|
||||
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', () => {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
Reference in new issue
Block a user