proschi 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +378 -52
- package/dist/render.cjs +212 -209
- package/dist/server.cjs +429 -62
- package/grammar/proschi.tmLanguage.json +113 -0
- package/package.json +6 -6
- package/schema/proschi-diagram.schema.json +132 -1
package/dist/server.cjs
CHANGED
|
@@ -25011,7 +25011,8 @@ var componentCatalog = [
|
|
|
25011
25011
|
{ type: "database", techStack: "Database", category: "Generic Components", searchTerms: "database relational sql db", aliases: ["DB", "RDBMS", "SQL Database", "Relational Database", "SQL"] },
|
|
25012
25012
|
{ type: "database", techStack: "NoSQL Database", category: "Generic Components", searchTerms: "nosql key value document wide column database", profile: "nosql", aliases: ["NoSQL", "NoSQL DB", "Key-Value Store", "KV Store", "Document Store", "Document Database", "Wide-Column Store"] },
|
|
25013
25013
|
{ type: "cache", techStack: "Cache", category: "Generic Components", searchTerms: "cache in-memory key value", aliases: ["In-Memory Cache", "Distributed Cache"] },
|
|
25014
|
-
{ type: "queue", techStack: "Message Queue", category: "Generic Components", searchTerms: "queue message
|
|
25014
|
+
{ type: "queue", techStack: "Message Queue", category: "Generic Components", searchTerms: "queue message job task", aliases: ["Queue", "Job Queue", "Task Queue"] },
|
|
25015
|
+
{ type: "broker", techStack: "Message Broker", category: "Generic Components", searchTerms: "message broker topic stream event bus log pub sub", aliases: ["Broker", "Event Bus", "Topic", "Event Stream", "Stream", "Event Log"] },
|
|
25015
25016
|
{ type: "storage", techStack: "Object Storage", category: "Generic Components", searchTerms: "object storage blob bucket files", aliases: ["Object Store", "Blob Store", "Blob Storage", "Bucket", "Storage", "File Storage"] },
|
|
25016
25017
|
{ type: "cdn", techStack: "CDN", category: "Generic Components", searchTerms: "cdn content delivery network edge cache", kind: "cdn", aliases: ["Content Delivery Network", "Edge Cache"] },
|
|
25017
25018
|
{ type: "cdn", techStack: "Load Balancer", category: "Generic Components", searchTerms: "load balancer lb reverse proxy", kind: "loadbalancer", aliases: ["LB", "Reverse Proxy", "L7 Load Balancer", "L4 Load Balancer"] },
|
|
@@ -25072,10 +25073,10 @@ var componentCatalog = [
|
|
|
25072
25073
|
{ type: "database", techStack: "AWS OpenSearch", category: "AWS Services", searchTerms: "aws opensearch elasticsearch search", kind: "search", aliases: ["Amazon OpenSearch", "OpenSearch Service", "AWS Elasticsearch"] },
|
|
25073
25074
|
// AWS Messaging
|
|
25074
25075
|
{ type: "queue", techStack: "AWS SQS", category: "AWS Services", searchTerms: "aws sqs queue message", aliases: ["Amazon SQS", "Simple Queue Service"] },
|
|
25075
|
-
{ type: "
|
|
25076
|
-
{ type: "
|
|
25077
|
-
{ type: "
|
|
25078
|
-
{ type: "
|
|
25076
|
+
{ type: "broker", techStack: "AWS SNS", category: "AWS Services", searchTerms: "aws sns notification topic pubsub", aliases: ["SNS", "Amazon SNS", "Simple Notification Service"] },
|
|
25077
|
+
{ type: "broker", techStack: "AWS EventBridge", category: "AWS Services", searchTerms: "aws eventbridge event bus", aliases: ["EventBridge", "Amazon EventBridge"] },
|
|
25078
|
+
{ type: "broker", techStack: "AWS Kinesis", category: "AWS Services", searchTerms: "aws kinesis stream data", aliases: ["Kinesis", "Amazon Kinesis", "Kinesis Data Streams"] },
|
|
25079
|
+
{ type: "broker", techStack: "AWS MSK", category: "AWS Services", searchTerms: "aws msk managed kafka stream", aliases: ["MSK", "Amazon MSK", "Managed Streaming for Kafka"] },
|
|
25079
25080
|
// AWS CDN/Network
|
|
25080
25081
|
{ type: "cdn", techStack: "AWS CloudFront", category: "AWS Services", searchTerms: "aws cloudfront cdn edge", kind: "cdn", aliases: ["CloudFront", "Amazon CloudFront"] },
|
|
25081
25082
|
{ type: "cdn", techStack: "AWS Route53", category: "AWS Services", searchTerms: "aws route53 dns domain", kind: "dns", aliases: ["Route53", "Amazon Route 53"] },
|
|
@@ -25103,7 +25104,7 @@ var componentCatalog = [
|
|
|
25103
25104
|
{ type: "database", techStack: "GCP BigQuery", category: "GCP Services", searchTerms: "gcp google bigquery data warehouse analytics", kind: "analytics", aliases: ["BigQuery", "Google BigQuery"] },
|
|
25104
25105
|
{ type: "cache", techStack: "GCP Memorystore", category: "GCP Services", searchTerms: "gcp google memorystore redis memcached cache", aliases: ["Memorystore"] },
|
|
25105
25106
|
// GCP Messaging
|
|
25106
|
-
{ type: "
|
|
25107
|
+
{ type: "broker", techStack: "GCP Pub/Sub", category: "GCP Services", searchTerms: "gcp google pubsub pub/sub message queue", aliases: ["Pub/Sub", "Google Pub/Sub", "Cloud Pub/Sub"] },
|
|
25107
25108
|
// GCP CDN/Network
|
|
25108
25109
|
{ type: "cdn", techStack: "GCP Cloud CDN", category: "GCP Services", searchTerms: "gcp google cloud cdn edge", kind: "cdn", aliases: ["Cloud CDN", "Google Cloud CDN"] },
|
|
25109
25110
|
{ type: "cdn", techStack: "GCP Cloud DNS", category: "GCP Services", searchTerms: "gcp google cloud dns domain", kind: "dns", aliases: ["Cloud DNS", "Google Cloud DNS"] },
|
|
@@ -25130,9 +25131,9 @@ var componentCatalog = [
|
|
|
25130
25131
|
{ type: "cache", techStack: "Azure Cache for Redis", category: "Azure Services", searchTerms: "azure redis cache", aliases: ["Azure Redis"] },
|
|
25131
25132
|
{ type: "database", techStack: "Azure AI Search", category: "Azure Services", searchTerms: "azure ai cognitive search", kind: "search", aliases: ["Azure Cognitive Search", "Azure Search"] },
|
|
25132
25133
|
// Azure Messaging
|
|
25133
|
-
{ type: "
|
|
25134
|
-
{ type: "
|
|
25135
|
-
{ type: "
|
|
25134
|
+
{ type: "broker", techStack: "Azure Service Bus", category: "Azure Services", searchTerms: "azure service bus queue message", aliases: ["Service Bus"] },
|
|
25135
|
+
{ type: "broker", techStack: "Azure Event Hubs", category: "Azure Services", searchTerms: "azure event hubs stream", aliases: ["Event Hubs", "Event Hub"] },
|
|
25136
|
+
{ type: "broker", techStack: "Azure Event Grid", category: "Azure Services", searchTerms: "azure event grid", aliases: ["Event Grid"] },
|
|
25136
25137
|
{ type: "queue", techStack: "Azure Queue Storage", category: "Azure Services", searchTerms: "azure queue storage message", aliases: ["Azure Queue", "Azure Queues"] },
|
|
25137
25138
|
// Azure CDN/Network
|
|
25138
25139
|
{ type: "cdn", techStack: "Azure CDN", category: "Azure Services", searchTerms: "azure cdn edge", kind: "cdn" },
|
|
@@ -25172,16 +25173,16 @@ var componentCatalog = [
|
|
|
25172
25173
|
{ type: "cache", techStack: "Valkey", category: "Cache & In-Memory", searchTerms: "valkey redis cache memory" },
|
|
25173
25174
|
{ type: "cache", techStack: "KeyDB", category: "Cache & In-Memory", searchTerms: "keydb redis cache memory" },
|
|
25174
25175
|
{ type: "cache", techStack: "Dragonfly", category: "Cache & In-Memory", searchTerms: "dragonfly dragonflydb redis cache memory", aliases: ["DragonflyDB"] },
|
|
25175
|
-
//
|
|
25176
|
-
{ type: "
|
|
25177
|
-
{ type: "
|
|
25178
|
-
{ type: "queue", techStack: "SQS", category: "
|
|
25179
|
-
{ type: "queue", techStack: "Redis Queue", category: "
|
|
25180
|
-
{ type: "
|
|
25181
|
-
{ type: "
|
|
25182
|
-
{ type: "
|
|
25183
|
-
{ type: "queue", techStack: "ZeroMQ", category: "
|
|
25184
|
-
{ type: "
|
|
25176
|
+
// Queues & Brokers: a queue hands each message to one consumer; a broker routes or logs it for many.
|
|
25177
|
+
{ type: "broker", techStack: "Kafka", category: "Queues & Brokers", searchTerms: "kafka message broker event stream log topic", aliases: ["Apache Kafka", "Confluent", "Confluent Kafka", "Kafka Topic"] },
|
|
25178
|
+
{ type: "broker", techStack: "RabbitMQ", category: "Queues & Brokers", searchTerms: "rabbitmq message broker queue amqp", aliases: ["Rabbit", "AMQP"] },
|
|
25179
|
+
{ type: "queue", techStack: "SQS", category: "Queues & Brokers", searchTerms: "sqs queue aws message" },
|
|
25180
|
+
{ type: "queue", techStack: "Redis Queue", category: "Queues & Brokers", searchTerms: "redis queue message", aliases: ["Redis Streams", "Redis Pub/Sub"] },
|
|
25181
|
+
{ type: "broker", techStack: "NATS", category: "Queues & Brokers", searchTerms: "nats message broker stream", aliases: ["NATS JetStream", "JetStream"] },
|
|
25182
|
+
{ type: "broker", techStack: "Apache Pulsar", category: "Queues & Brokers", searchTerms: "pulsar message broker stream", aliases: ["Pulsar"] },
|
|
25183
|
+
{ type: "broker", techStack: "ActiveMQ", category: "Queues & Brokers", searchTerms: "activemq message broker queue", aliases: ["Apache ActiveMQ", "Amazon MQ"] },
|
|
25184
|
+
{ type: "queue", techStack: "ZeroMQ", category: "Queues & Brokers", searchTerms: "zeromq zmq message queue", aliases: ["ZMQ", "0MQ"] },
|
|
25185
|
+
{ type: "broker", techStack: "Redpanda", category: "Queues & Brokers", searchTerms: "redpanda kafka message broker stream" },
|
|
25185
25186
|
// Search
|
|
25186
25187
|
{ type: "database", techStack: "OpenSearch", category: "Search", searchTerms: "opensearch elasticsearch search", kind: "search" },
|
|
25187
25188
|
{ type: "database", techStack: "Solr", category: "Search", searchTerms: "solr lucene search", kind: "search", aliases: ["Apache Solr"] },
|
|
@@ -25297,6 +25298,7 @@ var IDENT_PART = /[A-Za-z0-9_]/;
|
|
|
25297
25298
|
var TEAM_PART = /[A-Za-z0-9_-]/;
|
|
25298
25299
|
var DIGIT = /[0-9]/;
|
|
25299
25300
|
var LETTER = /[A-Za-z]/;
|
|
25301
|
+
var HEX_COLOR = /^#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})(?![A-Za-z0-9_])/;
|
|
25300
25302
|
var SEPARATE_UNIT = /^[ \t]+(rps|rpm|rpd|ms|s|%|usd\/month|usd\/GB|MB\/s|GB\/s)(?![A-Za-z0-9_/])/;
|
|
25301
25303
|
function tokenizeLine(text, line) {
|
|
25302
25304
|
const tokens = [];
|
|
@@ -25309,7 +25311,14 @@ function tokenizeLine(text, line) {
|
|
|
25309
25311
|
i++;
|
|
25310
25312
|
continue;
|
|
25311
25313
|
}
|
|
25312
|
-
if (ch === "#")
|
|
25314
|
+
if (ch === "#") {
|
|
25315
|
+
const prev = tokens[tokens.length - 1];
|
|
25316
|
+
const hex = prev?.kind === "ident" && prev.value === "color" ? HEX_COLOR.exec(text.slice(i)) : null;
|
|
25317
|
+
if (!hex) break;
|
|
25318
|
+
tokens.push({ kind: "hex", value: hex[0], col: i + 1, length: hex[0].length });
|
|
25319
|
+
i += hex[0].length;
|
|
25320
|
+
continue;
|
|
25321
|
+
}
|
|
25313
25322
|
const start = i;
|
|
25314
25323
|
if (ch === ":") {
|
|
25315
25324
|
const raw = stripTrailingComment(text.slice(i + 1));
|
|
@@ -25375,6 +25384,12 @@ function tokenizeLine(text, line) {
|
|
|
25375
25384
|
tokens.push({ kind: "ident", value: text.slice(start, i), col: start + 1, length: i - start });
|
|
25376
25385
|
continue;
|
|
25377
25386
|
}
|
|
25387
|
+
if (ch === "." && IDENT_START.test(text[i + 1] ?? "")) {
|
|
25388
|
+
i++;
|
|
25389
|
+
while (i < text.length && IDENT_PART.test(text[i])) i++;
|
|
25390
|
+
tokens.push({ kind: "tag", value: text.slice(start + 1, i), col: start + 1, length: i - start });
|
|
25391
|
+
continue;
|
|
25392
|
+
}
|
|
25378
25393
|
if (ch === "{" || ch === "}" || ch === ",") {
|
|
25379
25394
|
const kind = ch === "{" ? "lbrace" : ch === "}" ? "rbrace" : "comma";
|
|
25380
25395
|
tokens.push({ kind, value: ch, col: start + 1, length: 1 });
|
|
@@ -25513,10 +25528,12 @@ function classify(lines) {
|
|
|
25513
25528
|
entries2.push({ ...base, kind: "cells", section, cells });
|
|
25514
25529
|
continue;
|
|
25515
25530
|
}
|
|
25516
|
-
|
|
25517
|
-
|
|
25531
|
+
const tag = fourth?.kind === "tag" ? fourth : void 0;
|
|
25532
|
+
const labelToken = tokens[tag ? 4 : 3];
|
|
25533
|
+
if (!inSection && first.kind === "ident" && second?.kind === "arrow" && third?.kind === "ident" && tokens.length <= (tag ? 5 : 4) && (!labelToken || labelToken.kind === "label")) {
|
|
25534
|
+
const entry = { ...base, kind: "connection", from: first.value, arrow: second.value, to: third.value, tag: tag?.value, label: labelToken?.value, continuation: [] };
|
|
25518
25535
|
const brackets = new BracketCounter();
|
|
25519
|
-
if (
|
|
25536
|
+
if (labelToken && brackets.feed(labelToken.value) > 0) {
|
|
25520
25537
|
const delta = indent(depth).length - width(leadingSpace(raw));
|
|
25521
25538
|
while (i + 1 < lines.length) {
|
|
25522
25539
|
const next = lines[++i];
|
|
@@ -25529,7 +25546,7 @@ function classify(lines) {
|
|
|
25529
25546
|
continue;
|
|
25530
25547
|
}
|
|
25531
25548
|
const oneLineDecision = first.value === "decision" && second?.kind === "string" && third?.kind === "ident" && third.value === "because";
|
|
25532
|
-
if (!inSection && first.kind === "ident" && !KEYWORDS.has(first.value) && !oneLineDecision && tokens.slice(1).every((t) => ["string", "tech", "team", "ident", "number", "comma"].includes(t.kind))) {
|
|
25549
|
+
if (!inSection && first.kind === "ident" && !KEYWORDS.has(first.value) && !oneLineDecision && !isLayoutHint(tokens) && tokens.slice(1).every((t) => ["string", "tech", "team", "ident", "number", "comma", "tag", "hex"].includes(t.kind))) {
|
|
25533
25550
|
let k = 1;
|
|
25534
25551
|
const take = (kind) => tokens[k]?.kind === kind ? source(raw, tokens[k++]) : "";
|
|
25535
25552
|
const cells2 = [first.value, take("string"), take("tech"), take("team"), join(raw, tokens.slice(k))];
|
|
@@ -25547,6 +25564,10 @@ function classify(lines) {
|
|
|
25547
25564
|
}
|
|
25548
25565
|
return entries2;
|
|
25549
25566
|
}
|
|
25567
|
+
function isLayoutHint(tokens) {
|
|
25568
|
+
const [first, second] = tokens;
|
|
25569
|
+
return tokens.length === 2 && (first.value === "direction" || first.value === "layout") && second.kind === "ident" && !["pos", "critical", "color", "rank"].includes(second.value) && !/^x\d+$/.test(second.value);
|
|
25570
|
+
}
|
|
25550
25571
|
function sectionOpenedBy(tokens) {
|
|
25551
25572
|
const [first, second] = tokens;
|
|
25552
25573
|
if (first.kind !== "ident" || tokens[tokens.length - 1].kind !== "lbrace") return void 0;
|
|
@@ -25621,12 +25642,13 @@ function alignCells(run) {
|
|
|
25621
25642
|
}
|
|
25622
25643
|
function alignConnections(run) {
|
|
25623
25644
|
const head = Math.max(...run.map((e) => len(e.from) + 1 + e.arrow.length));
|
|
25645
|
+
const end = (e) => e.tag ? `${e.to} .${e.tag}` : e.to;
|
|
25624
25646
|
const labelled = run.filter((e) => e.label !== void 0);
|
|
25625
|
-
const target = labelled.length ? Math.max(...labelled.map((e) => len(e
|
|
25647
|
+
const target = labelled.length ? Math.max(...labelled.map((e) => len(end(e)))) : 0;
|
|
25626
25648
|
return run.map((e) => {
|
|
25627
25649
|
const start = e.from + " ".repeat(head - len(e.from) - e.arrow.length) + e.arrow + " ";
|
|
25628
|
-
if (e.label === void 0) return start + e
|
|
25629
|
-
return start + pad(e
|
|
25650
|
+
if (e.label === void 0) return start + end(e);
|
|
25651
|
+
return start + pad(end(e), target) + " :" + (e.label ? " " + e.label : "");
|
|
25630
25652
|
});
|
|
25631
25653
|
}
|
|
25632
25654
|
function commentOf(raw, tokens) {
|
|
@@ -25668,8 +25690,8 @@ var len = (s) => [...s].length;
|
|
|
25668
25690
|
var pad = (s, to) => s + " ".repeat(Math.max(0, to - len(s)));
|
|
25669
25691
|
|
|
25670
25692
|
// ../frontend/src/dsl/kinds.ts
|
|
25671
|
-
var KINDS = ["client", "edge", "cdn", "loadbalancer", "gateway", "dns", "service", "function", "cache", "database", "search", "analytics", "queue", "storage", "external", "other"];
|
|
25672
|
-
var DATA_STORE_KINDS = ["cache", "database", "search", "analytics", "queue", "storage"];
|
|
25693
|
+
var KINDS = ["client", "edge", "cdn", "loadbalancer", "gateway", "dns", "service", "function", "cache", "database", "search", "analytics", "queue", "broker", "storage", "external", "other"];
|
|
25694
|
+
var DATA_STORE_KINDS = ["cache", "database", "search", "analytics", "queue", "broker", "storage"];
|
|
25673
25695
|
var EDGE_KINDS = ["edge", "cdn", "loadbalancer", "gateway", "dns"];
|
|
25674
25696
|
var BY_TECH = new Map(componentCatalog.flatMap((c) => "kind" in c ? [[c.techStack, c.kind]] : []));
|
|
25675
25697
|
var BY_TYPE = {
|
|
@@ -25682,6 +25704,7 @@ var BY_TYPE = {
|
|
|
25682
25704
|
cache: "cache",
|
|
25683
25705
|
database: "database",
|
|
25684
25706
|
queue: "queue",
|
|
25707
|
+
broker: "broker",
|
|
25685
25708
|
storage: "storage",
|
|
25686
25709
|
external: "external"
|
|
25687
25710
|
};
|
|
@@ -25703,6 +25726,7 @@ var TYPE_OF_KIND = {
|
|
|
25703
25726
|
search: "database",
|
|
25704
25727
|
analytics: "database",
|
|
25705
25728
|
queue: "queue",
|
|
25729
|
+
broker: "broker",
|
|
25706
25730
|
storage: "storage",
|
|
25707
25731
|
external: "external",
|
|
25708
25732
|
other: "shape"
|
|
@@ -25715,7 +25739,8 @@ var KIND_WORDS = [
|
|
|
25715
25739
|
[/gateway|\bapim?\b.*manage/, "gateway"],
|
|
25716
25740
|
[/\bwaf\b|firewall/, "edge"],
|
|
25717
25741
|
[/cache|memcache/, "cache"],
|
|
25718
|
-
[/
|
|
25742
|
+
[/broker|event.?stream|event.?log|\bbus\b|topic|pub.?sub|event.?hub|kafka|kinesis|pulsar|redpanda|jetstream|\bnats\b/, "broker"],
|
|
25743
|
+
[/queue|\bmq\b|mq$/, "queue"],
|
|
25719
25744
|
[/search|lucene|\bindex\b/, "search"],
|
|
25720
25745
|
[/warehouse|analytic|olap|data.?lake|lakehouse|time.?series|\btsdb\b/, "analytics"],
|
|
25721
25746
|
[/bucket|blob|object.?stor|storage|\bfs\b|file.?system|\bnas\b|\bdisk\b|volume/, "storage"],
|
|
@@ -25736,7 +25761,7 @@ function isEdge(kind) {
|
|
|
25736
25761
|
return EDGE_KINDS.includes(kind);
|
|
25737
25762
|
}
|
|
25738
25763
|
function kindMatches(actual, wanted) {
|
|
25739
|
-
return actual === wanted || wanted === "edge" && isEdge(actual);
|
|
25764
|
+
return actual === wanted || wanted === "edge" && isEdge(actual) || wanted === "queue" && actual === "broker";
|
|
25740
25765
|
}
|
|
25741
25766
|
|
|
25742
25767
|
// ../frontend/src/dsl/paths.ts
|
|
@@ -25861,12 +25886,56 @@ function accessOf(httpMethod, label) {
|
|
|
25861
25886
|
return word && WRITE_VERBS.includes(word.toUpperCase()) ? "write" : "read";
|
|
25862
25887
|
}
|
|
25863
25888
|
|
|
25889
|
+
// ../frontend/src/dsl/palette.ts
|
|
25890
|
+
var PALETTE = {
|
|
25891
|
+
gray: "#6b7280",
|
|
25892
|
+
blue: "#3b82f6",
|
|
25893
|
+
teal: "#14b8a6",
|
|
25894
|
+
green: "#22c55e",
|
|
25895
|
+
yellow: "#eab308",
|
|
25896
|
+
orange: "#f97316",
|
|
25897
|
+
red: "#ef4444",
|
|
25898
|
+
purple: "#a855f7",
|
|
25899
|
+
pink: "#ec4899"
|
|
25900
|
+
};
|
|
25901
|
+
var COLOR_NAMES = Object.keys(PALETTE);
|
|
25902
|
+
var HEX = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
25903
|
+
function resolveColor(text) {
|
|
25904
|
+
if (Object.prototype.hasOwnProperty.call(PALETTE, text)) return PALETTE[text];
|
|
25905
|
+
if (!HEX.test(text)) return void 0;
|
|
25906
|
+
const digits = text.slice(1).toLowerCase();
|
|
25907
|
+
return "#" + (digits.length === 3 ? [...digits].map((d) => d + d).join("") : digits);
|
|
25908
|
+
}
|
|
25909
|
+
function suggestColor(text) {
|
|
25910
|
+
const word = text.toLowerCase();
|
|
25911
|
+
let best;
|
|
25912
|
+
for (const name of COLOR_NAMES) {
|
|
25913
|
+
const distance = editDistance2(word, name);
|
|
25914
|
+
if (distance <= 2 && (!best || distance < best.distance)) best = { name, distance };
|
|
25915
|
+
}
|
|
25916
|
+
return best?.name;
|
|
25917
|
+
}
|
|
25918
|
+
function editDistance2(a, b) {
|
|
25919
|
+
const row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
25920
|
+
for (let i = 1; i <= a.length; i++) {
|
|
25921
|
+
let prev = row[0];
|
|
25922
|
+
row[0] = i;
|
|
25923
|
+
for (let j = 1; j <= b.length; j++) {
|
|
25924
|
+
const next = row[j];
|
|
25925
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
25926
|
+
prev = next;
|
|
25927
|
+
}
|
|
25928
|
+
}
|
|
25929
|
+
return row[b.length];
|
|
25930
|
+
}
|
|
25931
|
+
|
|
25864
25932
|
// ../frontend/src/dsl/parser.ts
|
|
25865
25933
|
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
25866
25934
|
var DEFAULT_TECH = "Rectangle";
|
|
25867
25935
|
var DEFAULT_GROUP_TECH = "Logical Group";
|
|
25868
25936
|
var MAX_SCENARIOS = 32;
|
|
25869
25937
|
var MAX_ALT_DEPTH = 16;
|
|
25938
|
+
var DIRECTIONS = ["down", "right", "up", "left"];
|
|
25870
25939
|
var REPLICAS = /^x(\d+)$/;
|
|
25871
25940
|
var PERCENTILES = { p50: 50, p90: 90, p95: 95, p99: 99, p999: 99.9 };
|
|
25872
25941
|
var FIELD_FLAGS = ["key", "index", "unique", "optional"];
|
|
@@ -25895,6 +25964,7 @@ var GENERIC_TECH = {
|
|
|
25895
25964
|
search: "Search Engine",
|
|
25896
25965
|
analytics: "Data Warehouse",
|
|
25897
25966
|
queue: "Message Queue",
|
|
25967
|
+
broker: "Message Broker",
|
|
25898
25968
|
storage: "Object Storage",
|
|
25899
25969
|
external: "Third Party API",
|
|
25900
25970
|
other: "Note"
|
|
@@ -25940,6 +26010,12 @@ var Parser = class {
|
|
|
25940
26010
|
decisions = [];
|
|
25941
26011
|
tests = [];
|
|
25942
26012
|
blocks = [];
|
|
26013
|
+
legend = [];
|
|
26014
|
+
layout = {};
|
|
26015
|
+
/** Where each top-level layout hint was set, for the duplicate warning. */
|
|
26016
|
+
layoutLocs = {};
|
|
26017
|
+
/** `.tag` uses, resolved against the legend once every file is read. */
|
|
26018
|
+
tagRefs = [];
|
|
25943
26019
|
/** Checks of use case, scenario and node names, run once every file is read and use cases are built. */
|
|
25944
26020
|
deferred = [];
|
|
25945
26021
|
built = [];
|
|
@@ -25953,6 +26029,7 @@ var Parser = class {
|
|
|
25953
26029
|
run() {
|
|
25954
26030
|
this.parseFile();
|
|
25955
26031
|
this.createImplicitNodes();
|
|
26032
|
+
this.resolveTags();
|
|
25956
26033
|
for (const e of this.edges) this.connected.add(`${e.source}|${e.target}`).add(`${e.target}|${e.source}`);
|
|
25957
26034
|
const useCases = this.useCases.map(({ useCase, body }) => this.buildUseCase(useCase, body));
|
|
25958
26035
|
const endpoints = useCases.flatMap((u) => u.endpoint ? [u.endpoint] : []);
|
|
@@ -25971,7 +26048,9 @@ var Parser = class {
|
|
|
25971
26048
|
...this.capacity.length ? { capacity: this.capacity } : {},
|
|
25972
26049
|
...this.entities.length ? { entities: this.entities } : {},
|
|
25973
26050
|
...this.decisions.length ? { decisions: this.decisions } : {},
|
|
25974
|
-
...this.tests.length ? { tests: this.tests } : {}
|
|
26051
|
+
...this.tests.length ? { tests: this.tests } : {},
|
|
26052
|
+
...this.legend.length ? { legend: this.legend } : {},
|
|
26053
|
+
...Object.keys(this.layout).length ? { layout: this.layout } : {}
|
|
25975
26054
|
};
|
|
25976
26055
|
const order = (d) => this.fileOrder.get(d.file) ?? 0;
|
|
25977
26056
|
this.diagnostics.sort((a, b) => order(a) - order(b) || a.line - b.line || a.col - b.col);
|
|
@@ -26060,6 +26139,7 @@ var Parser = class {
|
|
|
26060
26139
|
case "traffic":
|
|
26061
26140
|
case "requirements":
|
|
26062
26141
|
case "capacity":
|
|
26142
|
+
case "legend":
|
|
26063
26143
|
if (second?.kind === "lbrace") this.openSection(first.value, tokens, line);
|
|
26064
26144
|
else this.parseNode(tokens, line);
|
|
26065
26145
|
break;
|
|
@@ -26076,6 +26156,15 @@ var Parser = class {
|
|
|
26076
26156
|
if (second?.kind === "string" && tokens[tokens.length - 1].kind === "lbrace") this.parseTest(tokens, line);
|
|
26077
26157
|
else this.parseNode(tokens, line);
|
|
26078
26158
|
break;
|
|
26159
|
+
// Keywords only as `direction <word>` and `layout <word>`, so a node may still be called direction or layout.
|
|
26160
|
+
case "direction":
|
|
26161
|
+
if (this.isHint(tokens)) this.parseDirection(tokens, line);
|
|
26162
|
+
else this.parseNode(tokens, line);
|
|
26163
|
+
break;
|
|
26164
|
+
case "layout":
|
|
26165
|
+
if (this.isHint(tokens)) this.parseLayoutMode(tokens, line);
|
|
26166
|
+
else this.parseNode(tokens, line);
|
|
26167
|
+
break;
|
|
26079
26168
|
default:
|
|
26080
26169
|
this.parseNode(tokens, line);
|
|
26081
26170
|
}
|
|
@@ -26094,6 +26183,86 @@ var Parser = class {
|
|
|
26094
26183
|
}
|
|
26095
26184
|
this.expectEnd(tokens, summary ? 3 : 2, line);
|
|
26096
26185
|
}
|
|
26186
|
+
/** `direction right`, `layout ordered`: a keyword and one plain word that is not a node part. */
|
|
26187
|
+
isHint(tokens) {
|
|
26188
|
+
const second = tokens[1];
|
|
26189
|
+
return tokens.length === 2 && second.kind === "ident" && !["pos", "critical", "color", "rank"].includes(second.value) && !REPLICAS.test(second.value);
|
|
26190
|
+
}
|
|
26191
|
+
/** `direction down|right|up|left`: at the top level for the whole diagram, inside a group for its members. */
|
|
26192
|
+
parseDirection(tokens, line) {
|
|
26193
|
+
const [keyword, value] = tokens;
|
|
26194
|
+
const loc = this.locOf(value, line);
|
|
26195
|
+
if (!DIRECTIONS.includes(value.value)) {
|
|
26196
|
+
this.error(`Unknown direction '${value.value}'; use direction down, right, up or left`, loc);
|
|
26197
|
+
return;
|
|
26198
|
+
}
|
|
26199
|
+
const direction = value.value;
|
|
26200
|
+
const top = this.top();
|
|
26201
|
+
if (top?.kind === "group") {
|
|
26202
|
+
const group = this.nodes.get(top.id);
|
|
26203
|
+
if (!group || group.loc !== top.loc) return;
|
|
26204
|
+
if (group.direction) this.warning(`Group '${group.id}' already has direction ${group.direction}; this one is ignored`, this.locOf(keyword, line));
|
|
26205
|
+
else group.direction = direction;
|
|
26206
|
+
return;
|
|
26207
|
+
}
|
|
26208
|
+
if (top) {
|
|
26209
|
+
this.error("direction is only allowed at the top level or inside a group", this.locOf(keyword, line));
|
|
26210
|
+
return;
|
|
26211
|
+
}
|
|
26212
|
+
if (this.file !== void 0) return;
|
|
26213
|
+
if (this.layoutLocs.direction) {
|
|
26214
|
+
this.warning(`direction is already set ${this.where(this.layoutLocs.direction, loc)}; this one is ignored`, this.locOf(keyword, line));
|
|
26215
|
+
return;
|
|
26216
|
+
}
|
|
26217
|
+
this.layout.direction = direction;
|
|
26218
|
+
this.layoutLocs.direction = loc;
|
|
26219
|
+
}
|
|
26220
|
+
/** `layout ordered`: lay edges out by declaration order. */
|
|
26221
|
+
parseLayoutMode(tokens, line) {
|
|
26222
|
+
const [keyword, value] = tokens;
|
|
26223
|
+
const loc = this.locOf(value, line);
|
|
26224
|
+
if (value.value !== "ordered") {
|
|
26225
|
+
this.error(`Unknown layout mode '${value.value}'; the only one is layout ordered`, loc);
|
|
26226
|
+
return;
|
|
26227
|
+
}
|
|
26228
|
+
if (this.top()) {
|
|
26229
|
+
this.error("layout ordered applies to the whole diagram; write it at the top level", this.locOf(keyword, line));
|
|
26230
|
+
return;
|
|
26231
|
+
}
|
|
26232
|
+
if (this.file !== void 0) return;
|
|
26233
|
+
if (this.layoutLocs.ordered) {
|
|
26234
|
+
this.warning(`layout ordered is already set ${this.where(this.layoutLocs.ordered, loc)}`, this.locOf(keyword, line));
|
|
26235
|
+
return;
|
|
26236
|
+
}
|
|
26237
|
+
this.layout.ordered = true;
|
|
26238
|
+
this.layoutLocs.ordered = loc;
|
|
26239
|
+
}
|
|
26240
|
+
/**
|
|
26241
|
+
* `rank 2`, `rank first`, `rank last` at `i` in a node or group header.
|
|
26242
|
+
* Returns how many tokens it used (0 when the token is something else), or
|
|
26243
|
+
* undefined after reporting an error.
|
|
26244
|
+
*/
|
|
26245
|
+
readRank(tokens, i, line, current) {
|
|
26246
|
+
const t = tokens[i];
|
|
26247
|
+
if (!isWord(t, "rank")) return 0;
|
|
26248
|
+
const value = tokens[i + 1];
|
|
26249
|
+
const help = "a layer number from 0 (rank 0, rank 1, \u2026), rank first or rank last";
|
|
26250
|
+
if (current.rank !== void 0) {
|
|
26251
|
+
this.error(`A node takes one rank; rank ${current.rank} is already set`, this.locOf(t, line));
|
|
26252
|
+
return void 0;
|
|
26253
|
+
}
|
|
26254
|
+
if (value?.kind === "number" && /^\d+$/.test(value.value)) {
|
|
26255
|
+
current.rank = Number(value.value);
|
|
26256
|
+
return 2;
|
|
26257
|
+
}
|
|
26258
|
+
if (value?.kind === "ident" && (value.value === "first" || value.value === "last")) {
|
|
26259
|
+
current.rank = value.value;
|
|
26260
|
+
return 2;
|
|
26261
|
+
}
|
|
26262
|
+
const numeric = value?.kind === "number" || value?.kind === "quantity";
|
|
26263
|
+
this.error(numeric ? `'${value.value}' is not a layer number; use ${help}` : `Expected ${help} after rank`, this.locOf(value ?? t, line));
|
|
26264
|
+
return void 0;
|
|
26265
|
+
}
|
|
26097
26266
|
parseImport(tokens, line) {
|
|
26098
26267
|
const keyword = tokens[0];
|
|
26099
26268
|
if (this.stack.length > 0) {
|
|
@@ -26167,14 +26336,29 @@ var Parser = class {
|
|
|
26167
26336
|
else this.warning(`'${tech.value}' is not a group style; using ${DEFAULT_GROUP_TECH}`, this.locOf(tech, line));
|
|
26168
26337
|
}
|
|
26169
26338
|
let position;
|
|
26170
|
-
|
|
26171
|
-
|
|
26172
|
-
|
|
26173
|
-
|
|
26174
|
-
|
|
26339
|
+
const style = {};
|
|
26340
|
+
const ranked = {};
|
|
26341
|
+
while (i < tokens.length && tokens[i].kind !== "lbrace") {
|
|
26342
|
+
const rankUsed = this.readRank(tokens, i, line, ranked);
|
|
26343
|
+
if (rankUsed === void 0) return;
|
|
26344
|
+
if (rankUsed) {
|
|
26345
|
+
i += rankUsed;
|
|
26346
|
+
continue;
|
|
26347
|
+
}
|
|
26348
|
+
if (tokens[i].kind === "ident" && tokens[i].value === "pos") {
|
|
26349
|
+
const [x, comma, y] = [tokens[i + 1], tokens[i + 2], tokens[i + 3]];
|
|
26350
|
+
if (x?.kind !== "number" || comma?.kind !== "comma" || y?.kind !== "number") {
|
|
26351
|
+
this.error("Expected pos x,y", this.locOf(tokens[i], line));
|
|
26352
|
+
return;
|
|
26353
|
+
}
|
|
26354
|
+
position = { x: Number(x.value), y: Number(y.value) };
|
|
26355
|
+
i += 4;
|
|
26356
|
+
continue;
|
|
26175
26357
|
}
|
|
26176
|
-
|
|
26177
|
-
|
|
26358
|
+
const used2 = this.readStylePart(tokens, i, line, style, false);
|
|
26359
|
+
if (used2 === void 0) return;
|
|
26360
|
+
if (used2 === 0) break;
|
|
26361
|
+
i += used2;
|
|
26178
26362
|
}
|
|
26179
26363
|
if (tokens[i]?.kind !== "lbrace") {
|
|
26180
26364
|
this.error("Expected { after group header", this.locOf(tokens[i] ?? tokens[i - 1], line));
|
|
@@ -26182,7 +26366,10 @@ var Parser = class {
|
|
|
26182
26366
|
}
|
|
26183
26367
|
this.expectEnd(tokens, i + 1, line);
|
|
26184
26368
|
const loc = this.locOf(idToken, line);
|
|
26185
|
-
|
|
26369
|
+
const group = { id: idToken.value, kind: "group", name, type: "group", techStack, parent: this.currentGroup(), position, loc };
|
|
26370
|
+
if (ranked.rank !== void 0) group.rank = ranked.rank;
|
|
26371
|
+
this.applyStyle(group, style);
|
|
26372
|
+
this.addNode(group);
|
|
26186
26373
|
this.stack.push({ kind: "group", id: idToken.value, loc });
|
|
26187
26374
|
}
|
|
26188
26375
|
parseUseCase(tokens, line) {
|
|
@@ -26287,8 +26474,16 @@ var Parser = class {
|
|
|
26287
26474
|
}
|
|
26288
26475
|
const node = { id: idToken.value, kind: "component", name: idToken.value, type: "shape", techStack: DEFAULT_TECH, parent: this.currentGroup(), loc };
|
|
26289
26476
|
let strings = 0;
|
|
26477
|
+
const style = {};
|
|
26290
26478
|
for (let i = 1; i < tokens.length; i++) {
|
|
26291
26479
|
const t = tokens[i];
|
|
26480
|
+
const styleUsed = this.readStylePart(tokens, i, line, style, true);
|
|
26481
|
+
const used2 = styleUsed === 0 ? this.readRank(tokens, i, line, node) : styleUsed;
|
|
26482
|
+
if (used2 === void 0) return;
|
|
26483
|
+
if (used2 > 0) {
|
|
26484
|
+
i += used2 - 1;
|
|
26485
|
+
continue;
|
|
26486
|
+
}
|
|
26292
26487
|
if (t.kind === "string" && strings < 2) {
|
|
26293
26488
|
if (strings++ === 0) node.name = t.value;
|
|
26294
26489
|
else node.description = t.value;
|
|
@@ -26323,16 +26518,23 @@ var Parser = class {
|
|
|
26323
26518
|
return;
|
|
26324
26519
|
}
|
|
26325
26520
|
}
|
|
26521
|
+
this.applyStyle(node, style);
|
|
26326
26522
|
this.addNode(node);
|
|
26327
26523
|
}
|
|
26328
26524
|
parseConnection(tokens, index) {
|
|
26329
26525
|
const line = index + 1;
|
|
26330
|
-
const [fromToken, arrowToken, toToken
|
|
26526
|
+
const [fromToken, arrowToken, toToken] = tokens;
|
|
26527
|
+
const tagToken = tokens[3]?.kind === "tag" ? tokens[3] : void 0;
|
|
26528
|
+
const labelToken = tokens[tagToken ? 4 : 3];
|
|
26331
26529
|
if (toToken?.kind !== "ident") {
|
|
26332
26530
|
this.error("Expected a target id after the arrow", this.locOf(toToken ?? arrowToken, line));
|
|
26333
26531
|
return index;
|
|
26334
26532
|
}
|
|
26335
|
-
if (
|
|
26533
|
+
if (tagToken && tokens[4]?.kind === "tag") {
|
|
26534
|
+
this.error("A connection takes one tag", this.locOf(tokens[4], line));
|
|
26535
|
+
return index;
|
|
26536
|
+
}
|
|
26537
|
+
if (tokens.length > (tagToken ? 5 : 4) || labelToken && labelToken.kind !== "label") {
|
|
26336
26538
|
this.error("Expected : before the connection label", this.locOf(labelToken, line));
|
|
26337
26539
|
return index;
|
|
26338
26540
|
}
|
|
@@ -26361,7 +26563,12 @@ var Parser = class {
|
|
|
26361
26563
|
const arrow = arrowToken.value;
|
|
26362
26564
|
this.references.push({ id: from, loc: this.locOf(fromToken, line) }, { id: to, loc: this.locOf(toToken, line) });
|
|
26363
26565
|
const top = this.top();
|
|
26364
|
-
|
|
26566
|
+
const inUseCase = top?.kind === "usecase" || top?.kind === "alt" || top?.kind === "par";
|
|
26567
|
+
if (tagToken && inUseCase) {
|
|
26568
|
+
this.error("Tags style connections in the architecture, not use case steps", this.locOf(tagToken, line));
|
|
26569
|
+
return lastLine;
|
|
26570
|
+
}
|
|
26571
|
+
if (inUseCase) {
|
|
26365
26572
|
const body = this.currentBody();
|
|
26366
26573
|
body.openAlt = void 0;
|
|
26367
26574
|
const prefixes = arrow === "-->" ? { rest: label } : this.readLabelPrefixes(label, labelToken, line);
|
|
@@ -26384,7 +26591,12 @@ var Parser = class {
|
|
|
26384
26591
|
const base = `${from}->${to}`;
|
|
26385
26592
|
const count = this.edgeCount.get(base) ?? 0;
|
|
26386
26593
|
this.edgeCount.set(base, count + 1);
|
|
26387
|
-
|
|
26594
|
+
const edge = { id: count ? `${base}#${count + 1}` : base, source: from, target: to, label: unquote(label) || void 0, loc };
|
|
26595
|
+
if (tagToken) {
|
|
26596
|
+
edge.tag = tagToken.value;
|
|
26597
|
+
this.tagRefs.push({ tag: tagToken.value, loc: this.locOf(tagToken, line), target: edge });
|
|
26598
|
+
}
|
|
26599
|
+
this.edges.push(edge);
|
|
26388
26600
|
}
|
|
26389
26601
|
return lastLine;
|
|
26390
26602
|
}
|
|
@@ -26438,7 +26650,7 @@ var Parser = class {
|
|
|
26438
26650
|
this.skipSection(section, tokens, line);
|
|
26439
26651
|
return false;
|
|
26440
26652
|
}
|
|
26441
|
-
if (section === "traffic" || section === "requirements" || section === "capacity") this.expectEnd(tokens, 2, line);
|
|
26653
|
+
if (section === "traffic" || section === "requirements" || section === "capacity" || section === "legend") this.expectEnd(tokens, 2, line);
|
|
26442
26654
|
this.stack.push({ kind: "section", section, ...fill, loc });
|
|
26443
26655
|
return true;
|
|
26444
26656
|
}
|
|
@@ -26457,6 +26669,8 @@ var Parser = class {
|
|
|
26457
26669
|
return this.parseRequirement(tokens, line);
|
|
26458
26670
|
case "capacity":
|
|
26459
26671
|
return this.parseCapacityLine(tokens, line);
|
|
26672
|
+
case "legend":
|
|
26673
|
+
return this.parseLegendLine(tokens, line);
|
|
26460
26674
|
case "entity":
|
|
26461
26675
|
return this.parseField(frame.entity, tokens, line);
|
|
26462
26676
|
case "decision":
|
|
@@ -26468,6 +26682,110 @@ var Parser = class {
|
|
|
26468
26682
|
}
|
|
26469
26683
|
}
|
|
26470
26684
|
}
|
|
26685
|
+
/**
|
|
26686
|
+
* Reads one style part of a node or group header at `i`: `color <name|#hex>`,
|
|
26687
|
+
* `.tag` and, on nodes, `critical`. Returns how many tokens it used (0 when
|
|
26688
|
+
* the token is something else), or undefined after reporting an error.
|
|
26689
|
+
*/
|
|
26690
|
+
readStylePart(tokens, i, line, parts, isNode) {
|
|
26691
|
+
const t = tokens[i];
|
|
26692
|
+
if (t.kind === "tag") {
|
|
26693
|
+
if (parts.tag) {
|
|
26694
|
+
this.error(`A ${isNode ? "node" : "group"} takes one tag; '.${parts.tag.value}' is already set`, this.locOf(t, line));
|
|
26695
|
+
return void 0;
|
|
26696
|
+
}
|
|
26697
|
+
parts.tag = { value: t.value, loc: this.locOf(t, line) };
|
|
26698
|
+
return 1;
|
|
26699
|
+
}
|
|
26700
|
+
if (isWord(t, "color")) {
|
|
26701
|
+
const value = this.readColor(tokens[i + 1], t, line);
|
|
26702
|
+
if (value === void 0) return void 0;
|
|
26703
|
+
parts.color = value;
|
|
26704
|
+
return 2;
|
|
26705
|
+
}
|
|
26706
|
+
if (isNode && isWord(t, "critical")) {
|
|
26707
|
+
parts.critical = true;
|
|
26708
|
+
return 1;
|
|
26709
|
+
}
|
|
26710
|
+
return 0;
|
|
26711
|
+
}
|
|
26712
|
+
/** The color in `token`, a palette name or `#hex`, or undefined after reporting an error. */
|
|
26713
|
+
readColor(token, after2, line) {
|
|
26714
|
+
const choices = `${COLOR_NAMES.join(", ")}, or a hex value after color, like color #3b82f6`;
|
|
26715
|
+
if (token?.kind !== "ident" && token?.kind !== "hex") {
|
|
26716
|
+
this.error(`Expected a color after ${after2.value}: ${choices}`, this.locOf(token ?? after2, line));
|
|
26717
|
+
return void 0;
|
|
26718
|
+
}
|
|
26719
|
+
const hex = resolveColor(token.value);
|
|
26720
|
+
if (!hex) {
|
|
26721
|
+
const suggestion = suggestColor(token.value);
|
|
26722
|
+
this.error(`Unknown color '${token.value}'.${suggestion ? ` Did you mean '${suggestion}'?` : ""} Use ${choices}`, this.locOf(token, line));
|
|
26723
|
+
return void 0;
|
|
26724
|
+
}
|
|
26725
|
+
return { name: token.value, hex };
|
|
26726
|
+
}
|
|
26727
|
+
applyStyle(node, parts) {
|
|
26728
|
+
if (parts.color) {
|
|
26729
|
+
node.color = parts.color.name;
|
|
26730
|
+
node.style = { color: parts.color.hex };
|
|
26731
|
+
}
|
|
26732
|
+
if (parts.critical) node.critical = true;
|
|
26733
|
+
if (parts.tag) {
|
|
26734
|
+
node.tag = parts.tag.value;
|
|
26735
|
+
this.tagRefs.push({ tag: parts.tag.value, loc: parts.tag.loc, target: node });
|
|
26736
|
+
}
|
|
26737
|
+
}
|
|
26738
|
+
/** `owned "Owned by Order Fulfillment" green dashed`: an id, a label, then a color and a dash in any order. */
|
|
26739
|
+
parseLegendLine(tokens, line) {
|
|
26740
|
+
const [idToken] = tokens;
|
|
26741
|
+
if (idToken.kind !== "ident") {
|
|
26742
|
+
this.error('Expected a legend entry, e.g. owned "Owned by us" green', this.locOf(idToken, line));
|
|
26743
|
+
return;
|
|
26744
|
+
}
|
|
26745
|
+
const loc = this.locOf(idToken, line);
|
|
26746
|
+
const entry = { id: idToken.value, label: idToken.value, loc };
|
|
26747
|
+
let i = 1;
|
|
26748
|
+
if (tokens[i]?.kind === "string") entry.label = tokens[i++].value;
|
|
26749
|
+
for (; i < tokens.length; i++) {
|
|
26750
|
+
const t = tokens[i];
|
|
26751
|
+
if (isWord(t, "dashed") || isWord(t, "dotted")) {
|
|
26752
|
+
entry.dash = t.value;
|
|
26753
|
+
continue;
|
|
26754
|
+
}
|
|
26755
|
+
if (t.kind === "ident") {
|
|
26756
|
+
const keyword = t.value === "color";
|
|
26757
|
+
const color = this.readColor(keyword ? tokens[i + 1] : t, t, line);
|
|
26758
|
+
if (!color) return;
|
|
26759
|
+
entry.colorName = color.name;
|
|
26760
|
+
entry.color = color.hex;
|
|
26761
|
+
if (keyword) i++;
|
|
26762
|
+
continue;
|
|
26763
|
+
}
|
|
26764
|
+
this.error(`Unexpected ${describe(t)} in legend entry; expected a color, dashed or dotted`, this.locOf(t, line));
|
|
26765
|
+
return;
|
|
26766
|
+
}
|
|
26767
|
+
const existing = this.legend.find((e) => e.id === entry.id);
|
|
26768
|
+
if (existing) {
|
|
26769
|
+
this.error(`Duplicate legend entry '${entry.id}' (first declared ${this.where(existing.loc, loc)})`, loc);
|
|
26770
|
+
return;
|
|
26771
|
+
}
|
|
26772
|
+
this.legend.push(entry);
|
|
26773
|
+
}
|
|
26774
|
+
/** Applies each `.tag` once every file is read; an unknown tag is an error. An explicit `color` wins over the tag's (D4). */
|
|
26775
|
+
resolveTags() {
|
|
26776
|
+
const entries2 = new Map(this.legend.map((e) => [e.id, e]));
|
|
26777
|
+
for (const ref of this.tagRefs) {
|
|
26778
|
+
const entry = entries2.get(ref.tag);
|
|
26779
|
+
if (!entry) {
|
|
26780
|
+
const close = [...entries2.keys()].find((id) => editDistance2(id, ref.tag) <= 2);
|
|
26781
|
+
const hint = close ? ` Did you mean '.${close}'?` : "";
|
|
26782
|
+
this.error(`Unknown tag '.${ref.tag}'; add it to the legend, e.g. legend { ${ref.tag} "What it means" blue }.${hint}`, ref.loc);
|
|
26783
|
+
continue;
|
|
26784
|
+
}
|
|
26785
|
+
const color = ref.target.style?.color ?? entry.color;
|
|
26786
|
+
if (color || entry.dash) ref.target.style = { ...color ? { color } : {}, ...entry.dash ? { dash: entry.dash } : {} };
|
|
26787
|
+
}
|
|
26788
|
+
}
|
|
26471
26789
|
/** `"Redirect" 100k rps mix "Cache hit" 90%, "Cache miss" 10%` */
|
|
26472
26790
|
parseTrafficLine(tokens, line) {
|
|
26473
26791
|
const [name, rate] = tokens;
|
|
@@ -27429,7 +27747,7 @@ function parseSize(text) {
|
|
|
27429
27747
|
return { value };
|
|
27430
27748
|
}
|
|
27431
27749
|
function inferProtocol(isAsync, method, target) {
|
|
27432
|
-
if (isAsync || target?.type === "queue") return "MESSAGING";
|
|
27750
|
+
if (isAsync || target?.type === "queue" || target?.type === "broker") return "MESSAGING";
|
|
27433
27751
|
if (!method) return "OTHER";
|
|
27434
27752
|
switch (target?.techStack) {
|
|
27435
27753
|
case "GraphQL":
|
|
@@ -27454,6 +27772,8 @@ function describe(token) {
|
|
|
27454
27772
|
return `[${token.value}]`;
|
|
27455
27773
|
case "team":
|
|
27456
27774
|
return `@${token.value}`;
|
|
27775
|
+
case "tag":
|
|
27776
|
+
return `tag '.${token.value}'`;
|
|
27457
27777
|
case "label":
|
|
27458
27778
|
return "':'";
|
|
27459
27779
|
case "lbrace":
|
|
@@ -27933,7 +28253,7 @@ function stepAccess(step) {
|
|
|
27933
28253
|
return step.access ?? accessOf(step.httpMethod, step.stepName ?? "");
|
|
27934
28254
|
}
|
|
27935
28255
|
function accessIn(diagram) {
|
|
27936
|
-
const queues = new Set(diagram.nodes.filter((n) => kindOf(n)
|
|
28256
|
+
const queues = new Set(diagram.nodes.filter((n) => kindMatches(kindOf(n), "queue")).map((n) => n.id));
|
|
27937
28257
|
return (step) => queues.has(step.toServiceId) ? "write" : stepAccess(step);
|
|
27938
28258
|
}
|
|
27939
28259
|
|
|
@@ -27952,6 +28272,7 @@ var RELATIONAL = {
|
|
|
27952
28272
|
durable: true,
|
|
27953
28273
|
consistency: "strong"
|
|
27954
28274
|
};
|
|
28275
|
+
var QUEUE = { rps: 5e4, latencyMs: 5, availability: 0.9999, costUsd: 200, durable: true, consistency: "strong" };
|
|
27955
28276
|
var KIND_PROFILES = {
|
|
27956
28277
|
client: { ...NEUTRAL, bandwidthMBps: 10 },
|
|
27957
28278
|
// A firewall or accelerator in front of everything else (WAF, Global Accelerator); `any edge` also selects the four below.
|
|
@@ -27967,7 +28288,9 @@ var KIND_PROFILES = {
|
|
|
27967
28288
|
database: RELATIONAL,
|
|
27968
28289
|
search: { rps: 3e3, latencyMs: 15, availability: 0.999, costUsd: 400, durable: true, consistency: "eventual" },
|
|
27969
28290
|
analytics: { rps: 200, latencyMs: 500, availability: 0.999, costUsd: 300, durable: true, consistency: "eventual" },
|
|
27970
|
-
queue:
|
|
28291
|
+
queue: QUEUE,
|
|
28292
|
+
// Same numbers as a queue for now: both take a message and hand it on later.
|
|
28293
|
+
broker: QUEUE,
|
|
27971
28294
|
storage: { rps: 5e3, latencyMs: 30, availability: 0.9999, costUsd: 50, durable: true, consistency: "eventual" },
|
|
27972
28295
|
external: { rps: 1e3, latencyMs: 200, availability: 0.999, costUsd: 0, durable: false },
|
|
27973
28296
|
other: NEUTRAL
|
|
@@ -28610,8 +28933,10 @@ function overUseCases(run, name, check, limit, anyWithoutTraffic = false) {
|
|
|
28610
28933
|
return fail(failed.length === 1 ? failed[0].message : `${failed.map((c) => c.value ?? c.message).join("; ")} ${limit}`, failed[0].hint);
|
|
28611
28934
|
}
|
|
28612
28935
|
function unknownUseCase(run, name) {
|
|
28613
|
-
const
|
|
28614
|
-
|
|
28936
|
+
const lower = name.toLowerCase();
|
|
28937
|
+
const near = run.diagram.useCases.filter((u) => editDistance(u.name.toLowerCase(), lower) <= Math.max(1, Math.floor(name.length / 4)));
|
|
28938
|
+
const hint = near.length ? `Use one of ${near.map((u) => quote(u.name)).join(", ")}` : `Add usecase ${quote(name)} { \u2026 }`;
|
|
28939
|
+
return { ...fail(`No use case named ${quote(name)}`, hint), missing: `use case ${name}` };
|
|
28615
28940
|
}
|
|
28616
28941
|
function saturatedOn(run, ids, value, maxMs) {
|
|
28617
28942
|
const n = [...new Set(ids)].map((id) => run.nodes.get(id)).find((x) => !!x?.saturated);
|
|
@@ -29086,6 +29411,10 @@ var KEYWORDS2 = [
|
|
|
29086
29411
|
{ label: "usecase", detail: 'usecase "Name" { steps }', snippet: 'usecase "${1:Name}" {\n $0\n}' },
|
|
29087
29412
|
{ label: "par", detail: "Steps that run in parallel", snippet: "par {\n $0\n}" },
|
|
29088
29413
|
{ label: "alt", detail: "One scenario of a use case", snippet: 'alt "${1:Scenario}" {\n $0\n}' },
|
|
29414
|
+
{ label: "legend", detail: 'Styles that .tag applies: owned "Owned by us" green', snippet: 'legend {\n ${1:tag} "${2:What it means}" ${3:blue}$0\n}' },
|
|
29415
|
+
// Layout hints (docs/LANGUAGE.md, "Steering the layout").
|
|
29416
|
+
{ label: "direction", detail: "Where edges flow: down, right, up or left (top level or in a group)", snippet: "direction ${1|down,right,up,left|}" },
|
|
29417
|
+
{ label: "layout ordered", detail: "Lay edges out by declaration order (top level)", snippet: "layout ordered" },
|
|
29089
29418
|
// High-level design sections (top level).
|
|
29090
29419
|
{ label: "traffic", detail: 'Requests per use case: "Use case" 100 rps', snippet: 'traffic {\n "${1:Use case}" ${2:100 rps}$0\n}' },
|
|
29091
29420
|
{ label: "requirements", detail: "Latency, availability, durability, resilience, cost", snippet: "requirements {\n p99 < ${1:200ms}$0\n}" },
|
|
@@ -29217,17 +29546,40 @@ function inLabel(before2) {
|
|
|
29217
29546
|
return false;
|
|
29218
29547
|
}
|
|
29219
29548
|
function insidePayload(lines, line) {
|
|
29549
|
+
return payloadLines(lines.slice(0, line + 1))[line];
|
|
29550
|
+
}
|
|
29551
|
+
function payloadLines(lines) {
|
|
29220
29552
|
let depth = 0;
|
|
29221
|
-
|
|
29222
|
-
const
|
|
29223
|
-
const colon =
|
|
29224
|
-
if (
|
|
29225
|
-
|
|
29226
|
-
|
|
29227
|
-
|
|
29553
|
+
return lines.map((text) => {
|
|
29554
|
+
const inside = depth > 0;
|
|
29555
|
+
const colon = inside ? -1 : labelStart(text);
|
|
29556
|
+
if (inside || colon >= 0) {
|
|
29557
|
+
for (const ch of text.slice(colon + 1)) {
|
|
29558
|
+
if (ch === "{" || ch === "[") depth++;
|
|
29559
|
+
else if (ch === "}" || ch === "]") depth = Math.max(0, depth - 1);
|
|
29560
|
+
}
|
|
29228
29561
|
}
|
|
29229
|
-
|
|
29230
|
-
|
|
29562
|
+
return inside;
|
|
29563
|
+
});
|
|
29564
|
+
}
|
|
29565
|
+
var TOKEN = /"(?:[^"\\]|\\.)*"?|(?<=\bcolor\s+)#[0-9a-fA-F]{3,6}(?![A-Za-z0-9_])|#.*|:.*|->>|-->|->|-x(?![A-Za-z0-9_])|\[[^\]\n]*\]|@[A-Za-z0-9_-]+|\.[A-Za-z_][A-Za-z0-9_]*|[A-Za-z_][A-Za-z0-9_]*/g;
|
|
29566
|
+
function semanticTokens(analysis) {
|
|
29567
|
+
const nodes = new Set(analysis.diagram.nodes.map((n) => n.id));
|
|
29568
|
+
const legend = analysis.diagram.legend ?? [];
|
|
29569
|
+
const tags = new Set(legend.map((e) => e.id));
|
|
29570
|
+
const entryAt = new Map(legend.filter((e) => e.loc.file === void 0).map((e) => [e.loc.line - 1, e.loc.col - 1]));
|
|
29571
|
+
const payload = payloadLines(analysis.lines);
|
|
29572
|
+
const out = [];
|
|
29573
|
+
analysis.lines.forEach((text, line) => {
|
|
29574
|
+
if (payload[line]) return;
|
|
29575
|
+
for (const m of text.matchAll(TOKEN)) {
|
|
29576
|
+
const word = m[0];
|
|
29577
|
+
const at = m.index ?? 0;
|
|
29578
|
+
const kind = word[0] === "[" ? "tech" : word[0] === "@" ? "team" : word[0] === "." ? tags.has(word.slice(1)) ? "tag" : null : entryAt.get(line) === at ? "tag" : /^[A-Za-z_]/.test(word) && nodes.has(word) ? "node" : null;
|
|
29579
|
+
if (kind) out.push({ line, character: at, length: word.length, kind });
|
|
29580
|
+
}
|
|
29581
|
+
});
|
|
29582
|
+
return out;
|
|
29231
29583
|
}
|
|
29232
29584
|
function labelStart(text) {
|
|
29233
29585
|
let quoted = false;
|
|
@@ -29432,7 +29784,7 @@ function positionAt(text, offset) {
|
|
|
29432
29784
|
|
|
29433
29785
|
// src/server.ts
|
|
29434
29786
|
if (process.argv.includes("--version")) {
|
|
29435
|
-
console.log(true ? "0.
|
|
29787
|
+
console.log(true ? "0.12.0" : "dev");
|
|
29436
29788
|
process.exit(0);
|
|
29437
29789
|
}
|
|
29438
29790
|
var connection = (0, import_node.createConnection)(import_node.ProposedFeatures.all);
|
|
@@ -29461,6 +29813,8 @@ function publishDependents(uri) {
|
|
|
29461
29813
|
validate(doc);
|
|
29462
29814
|
}
|
|
29463
29815
|
}
|
|
29816
|
+
var TOKEN_TYPE = { node: "variable", tech: "type", team: "decorator", tag: "enumMember" };
|
|
29817
|
+
var TOKEN_TYPES = Object.values(TOKEN_TYPE);
|
|
29464
29818
|
connection.onInitialize(() => ({
|
|
29465
29819
|
capabilities: {
|
|
29466
29820
|
textDocumentSync: import_node.TextDocumentSyncKind.Incremental,
|
|
@@ -29471,9 +29825,10 @@ connection.onInitialize(() => ({
|
|
|
29471
29825
|
documentSymbolProvider: true,
|
|
29472
29826
|
documentLinkProvider: { resolveProvider: false },
|
|
29473
29827
|
documentFormattingProvider: true,
|
|
29474
|
-
codeActionProvider: { codeActionKinds: [import_node.CodeActionKind.QuickFix] }
|
|
29828
|
+
codeActionProvider: { codeActionKinds: [import_node.CodeActionKind.QuickFix] },
|
|
29829
|
+
semanticTokensProvider: { legend: { tokenTypes: TOKEN_TYPES, tokenModifiers: [] }, full: true }
|
|
29475
29830
|
},
|
|
29476
|
-
serverInfo: { name: "proschi-language-server", version: true ? "0.
|
|
29831
|
+
serverInfo: { name: "proschi-language-server", version: true ? "0.12.0" : "dev" }
|
|
29477
29832
|
}));
|
|
29478
29833
|
function validate(document) {
|
|
29479
29834
|
const analysis = analysisOf(document);
|
|
@@ -29618,5 +29973,17 @@ connection.onCodeAction((params) => {
|
|
|
29618
29973
|
}
|
|
29619
29974
|
return actions;
|
|
29620
29975
|
});
|
|
29976
|
+
connection.languages.semanticTokens.on((params) => {
|
|
29977
|
+
const doc = documents.get(params.textDocument.uri);
|
|
29978
|
+
const data = [];
|
|
29979
|
+
let line = 0;
|
|
29980
|
+
let character = 0;
|
|
29981
|
+
for (const t of doc ? semanticTokens(analysisOf(doc)) : []) {
|
|
29982
|
+
data.push(t.line - line, t.line === line ? t.character - character : t.character, t.length, TOKEN_TYPES.indexOf(TOKEN_TYPE[t.kind]), 0);
|
|
29983
|
+
line = t.line;
|
|
29984
|
+
character = t.character;
|
|
29985
|
+
}
|
|
29986
|
+
return { data };
|
|
29987
|
+
});
|
|
29621
29988
|
documents.listen(connection);
|
|
29622
29989
|
connection.listen();
|