Merge pull request 'Add New Diagram command with starter templates' (#12) from feat/new-diagram-command into main
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
- **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
|
## 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.
|
- **`.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.
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ This is an unofficial extension. It bundles the Archify renderer (MIT, © tt-a1i
|
|||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
|
The fastest start: right-click a folder in the Explorer and choose **New Archify 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`:
|
1. Create a file ending in `.archify`, for example `hello.archify`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -41,6 +45,7 @@ Archify's own JSON naming, `name.architecture.json`, `name.workflow.json` and so
|
|||||||
|
|
||||||
## Features
|
## 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.
|
- **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.
|
- **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.
|
- **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.
|
||||||
@@ -48,7 +53,7 @@ Archify's own JSON naming, `name.architecture.json`, `name.workflow.json` and so
|
|||||||
- **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.
|
- **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.
|
||||||
- **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.
|
- **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.
|
- **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
|
## Source evidence
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,12 @@
|
|||||||
],
|
],
|
||||||
"contributes": {
|
"contributes": {
|
||||||
"commands": [
|
"commands": [
|
||||||
|
{
|
||||||
|
"command": "archify.newDiagram",
|
||||||
|
"title": "New Diagram\u2026",
|
||||||
|
"category": "Archify",
|
||||||
|
"icon": "$(new-file)"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "archify.showPreview",
|
"command": "archify.showPreview",
|
||||||
"title": "Open Preview",
|
"title": "Open Preview",
|
||||||
@@ -145,6 +151,11 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"explorer/context": [
|
"explorer/context": [
|
||||||
|
{
|
||||||
|
"command": "archify.newDiagram",
|
||||||
|
"when": "explorerResourceIsFolder",
|
||||||
|
"group": "navigation@10"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "archify.showPreview",
|
"command": "archify.showPreview",
|
||||||
"when": "resourceFilename =~ /(\\.archify|\\.(architecture|workflow|sequence|dataflow|lifecycle)\\.json)$/",
|
"when": "resourceFilename =~ /(\\.archify|\\.(architecture|workflow|sequence|dataflow|lifecycle)\\.json)$/",
|
||||||
@@ -167,6 +178,12 @@
|
|||||||
"when": "resourceFilename =~ /(\\.archify|\\.(architecture|workflow|sequence|dataflow|lifecycle)\\.json)$/",
|
"when": "resourceFilename =~ /(\\.archify|\\.(architecture|workflow|sequence|dataflow|lifecycle)\\.json)$/",
|
||||||
"group": "1_open"
|
"group": "1_open"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"file/newFile": [
|
||||||
|
{
|
||||||
|
"command": "archify.newDiagram",
|
||||||
|
"group": "file"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"keybindings": [
|
"keybindings": [
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { HTML_VIEW_TYPE, HtmlViewerProvider } from './htmlViewer';
|
|||||||
import { DIAGRAM_EDITOR_VIEW_TYPE, PreviewManager } from './preview';
|
import { DIAGRAM_EDITOR_VIEW_TYPE, PreviewManager } from './preview';
|
||||||
import { isEvidenceError, renderDiagram, renderToFile } from './renderer';
|
import { isEvidenceError, renderDiagram, renderToFile } from './renderer';
|
||||||
import { clearGitRootCache, evidenceMode, log, readText, runtimeFor } from './runtime';
|
import { clearGitRootCache, evidenceMode, log, readText, runtimeFor } from './runtime';
|
||||||
|
import { diagramFromTemplate, diagramName, invalidNameReason, TEMPLATES } from './templates';
|
||||||
|
|
||||||
const JSON_LANGUAGES = new Set(['json', 'jsonc']);
|
const JSON_LANGUAGES = new Set(['json', 'jsonc']);
|
||||||
|
|
||||||
@@ -61,6 +62,26 @@ export function activate(context: vscode.ExtensionContext): void {
|
|||||||
const openHtmlViewer = (uri: vscode.Uri, viewColumn?: vscode.ViewColumn) =>
|
const openHtmlViewer = (uri: vscode.Uri, viewColumn?: vscode.ViewColumn) =>
|
||||||
vscode.commands.executeCommand('vscode.openWith', uri, HTML_VIEW_TYPE, 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(
|
context.subscriptions.push(
|
||||||
vscode.commands.registerCommand('archify.showPreview', (arg?: unknown) => {
|
vscode.commands.registerCommand('archify.showPreview', (arg?: unknown) => {
|
||||||
const uri = sourceUri(arg);
|
const uri = sourceUri(arg);
|
||||||
@@ -88,6 +109,44 @@ export function activate(context: vscode.ExtensionContext): void {
|
|||||||
if (uri) return openHtmlViewer(uri);
|
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: 'New Archify 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: 'New Archify 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: 'New Archify 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.refreshPreview', () => previews.activePreview?.update(true)),
|
||||||
|
|
||||||
vscode.commands.registerCommand('archify.showSource', async () => {
|
vscode.commands.registerCommand('archify.showSource', async () => {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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}`);
|
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 check('opens a live preview beside the source', async () => {
|
||||||
await vscode.commands.executeCommand('archify.showPreviewToSide', workspaceFile('production-deployment.architecture.json'));
|
await vscode.commands.executeCommand('archify.showPreviewToSide', workspaceFile('production-deployment.architecture.json'));
|
||||||
const tabs = await waitFor('preview tab', () => {
|
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