@fairfox/polly 0.73.0 → 0.74.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/src/mesh.js +18 -12
- package/dist/src/mesh.js.map +5 -4
- package/dist/src/polly-ui/ActionSelect.d.ts +7 -3
- package/dist/src/polly-ui/Dropdown.d.ts +15 -0
- package/dist/src/polly-ui/Select.d.ts +2 -0
- package/dist/src/polly-ui/index.css +31 -14
- package/dist/src/polly-ui/index.js +124 -25
- package/dist/src/polly-ui/index.js.map +5 -5
- package/dist/src/polly-ui/styles.css +31 -14
- package/dist/src/shared/lib/derive-document-id.d.ts +21 -0
- package/dist/src/shared/lib/mesh-state.d.ts +6 -6
- package/dist/tools/verify/src/cli.js +25 -2
- package/dist/tools/verify/src/cli.js.map +3 -3
- package/dist/tools/visualize/src/cli.js +473 -23
- package/dist/tools/visualize/src/cli.js.map +9 -7
- package/package.json +1 -1
|
@@ -667,7 +667,7 @@ function detectProjectConfig(projectRoot) {
|
|
|
667
667
|
var init_project_detector = () => {};
|
|
668
668
|
|
|
669
669
|
// tools/visualize/src/cli.ts
|
|
670
|
-
import * as
|
|
670
|
+
import * as fs7 from "node:fs";
|
|
671
671
|
import * as path6 from "node:path";
|
|
672
672
|
|
|
673
673
|
// tools/analysis/src/extract/architecture.ts
|
|
@@ -1864,14 +1864,37 @@ class HandlerExtractor {
|
|
|
1864
1864
|
return null;
|
|
1865
1865
|
const key = keyArg.getLiteralValue();
|
|
1866
1866
|
const variableName = this.getVariableNameFromParent(node) || key;
|
|
1867
|
+
const access = this.extractMeshAccess(args[2]);
|
|
1867
1868
|
return {
|
|
1868
1869
|
kind,
|
|
1869
1870
|
key,
|
|
1870
1871
|
variableName,
|
|
1871
1872
|
filePath,
|
|
1872
|
-
line: node.getStartLineNumber()
|
|
1873
|
+
line: node.getStartLineNumber(),
|
|
1874
|
+
...access ? { access } : {}
|
|
1873
1875
|
};
|
|
1874
1876
|
}
|
|
1877
|
+
extractMeshAccess(optionsArg) {
|
|
1878
|
+
if (!optionsArg || !Node4.isObjectLiteralExpression(optionsArg))
|
|
1879
|
+
return;
|
|
1880
|
+
const accessProp = optionsArg.getProperty("access");
|
|
1881
|
+
if (!accessProp || !Node4.isPropertyAssignment(accessProp))
|
|
1882
|
+
return;
|
|
1883
|
+
const accessObj = accessProp.getInitializer();
|
|
1884
|
+
if (!accessObj || !Node4.isObjectLiteralExpression(accessObj))
|
|
1885
|
+
return;
|
|
1886
|
+
const predicateText = (name) => {
|
|
1887
|
+
const prop = accessObj.getProperty(name);
|
|
1888
|
+
if (!prop || !Node4.isPropertyAssignment(prop))
|
|
1889
|
+
return;
|
|
1890
|
+
return prop.getInitializer()?.getText().replace(/\s+/g, " ").trim();
|
|
1891
|
+
};
|
|
1892
|
+
const read = predicateText("read");
|
|
1893
|
+
const write = predicateText("write");
|
|
1894
|
+
if (read === undefined && write === undefined)
|
|
1895
|
+
return;
|
|
1896
|
+
return { read: read ?? "unset", write: write ?? "unset" };
|
|
1897
|
+
}
|
|
1875
1898
|
analyzeFileAndImports(sourceFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, globalStateConstraints, verifiedStates, resources) {
|
|
1876
1899
|
const filePath = sourceFile.getFilePath();
|
|
1877
1900
|
if (this.analyzedFiles.has(filePath)) {
|
|
@@ -4567,6 +4590,122 @@ async function analyzeArchitecture(options) {
|
|
|
4567
4590
|
return analyzer.analyze();
|
|
4568
4591
|
}
|
|
4569
4592
|
|
|
4593
|
+
// src/shared/lib/derive-document-id.ts
|
|
4594
|
+
import {
|
|
4595
|
+
interpretAsDocumentId
|
|
4596
|
+
} from "@automerge/automerge-repo/slim";
|
|
4597
|
+
import nacl from "tweetnacl";
|
|
4598
|
+
var DOC_ID_DOMAIN = "polly/meshState/v1";
|
|
4599
|
+
var keyEncoder = new TextEncoder;
|
|
4600
|
+
function deriveDocumentId(key) {
|
|
4601
|
+
const digest = nacl.hash(keyEncoder.encode(`${DOC_ID_DOMAIN}:${key}`));
|
|
4602
|
+
const bytes = digest.slice(0, 16);
|
|
4603
|
+
return interpretAsDocumentId(bytes);
|
|
4604
|
+
}
|
|
4605
|
+
|
|
4606
|
+
// tools/visualize/src/mesh-snapshot.ts
|
|
4607
|
+
import * as fs5 from "node:fs";
|
|
4608
|
+
|
|
4609
|
+
class MeshSnapshotError extends Error {
|
|
4610
|
+
constructor(message) {
|
|
4611
|
+
super(message);
|
|
4612
|
+
this.name = "MeshSnapshotError";
|
|
4613
|
+
}
|
|
4614
|
+
}
|
|
4615
|
+
function isObject(value) {
|
|
4616
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4617
|
+
}
|
|
4618
|
+
function requireStringArray(value, path5) {
|
|
4619
|
+
if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) {
|
|
4620
|
+
throw new MeshSnapshotError(`${path5} must be an array of strings`);
|
|
4621
|
+
}
|
|
4622
|
+
return value;
|
|
4623
|
+
}
|
|
4624
|
+
function validateHandle(value, path5) {
|
|
4625
|
+
if (!isObject(value)) {
|
|
4626
|
+
throw new MeshSnapshotError(`${path5} must be an object`);
|
|
4627
|
+
}
|
|
4628
|
+
if (typeof value["docSynchronizerExists"] !== "boolean") {
|
|
4629
|
+
throw new MeshSnapshotError(`${path5}.docSynchronizerExists must be a boolean`);
|
|
4630
|
+
}
|
|
4631
|
+
const knowsPeer = value["docSynchronizerKnowsPeer"];
|
|
4632
|
+
if (knowsPeer !== undefined && typeof knowsPeer !== "boolean") {
|
|
4633
|
+
throw new MeshSnapshotError(`${path5}.docSynchronizerKnowsPeer must be a boolean or absent`);
|
|
4634
|
+
}
|
|
4635
|
+
const status = value["peerDocumentStatus"];
|
|
4636
|
+
if (status !== undefined && typeof status !== "string") {
|
|
4637
|
+
throw new MeshSnapshotError(`${path5}.peerDocumentStatus must be a string or absent`);
|
|
4638
|
+
}
|
|
4639
|
+
return value;
|
|
4640
|
+
}
|
|
4641
|
+
function validatePeer(value, path5) {
|
|
4642
|
+
if (!isObject(value)) {
|
|
4643
|
+
throw new MeshSnapshotError(`${path5} must be an object`);
|
|
4644
|
+
}
|
|
4645
|
+
if (typeof value["peerId"] !== "string") {
|
|
4646
|
+
throw new MeshSnapshotError(`${path5}.peerId must be a string`);
|
|
4647
|
+
}
|
|
4648
|
+
const slot = value["slot"];
|
|
4649
|
+
if (slot !== undefined && slot !== null) {
|
|
4650
|
+
if (!isObject(slot)) {
|
|
4651
|
+
throw new MeshSnapshotError(`${path5}.slot must be an object or absent`);
|
|
4652
|
+
}
|
|
4653
|
+
const handles = slot["handles"];
|
|
4654
|
+
if (!isObject(handles)) {
|
|
4655
|
+
throw new MeshSnapshotError(`${path5}.slot.handles must be an object`);
|
|
4656
|
+
}
|
|
4657
|
+
for (const [docId, handle] of Object.entries(handles)) {
|
|
4658
|
+
validateHandle(handle, `${path5}.slot.handles[${docId}]`);
|
|
4659
|
+
}
|
|
4660
|
+
}
|
|
4661
|
+
return value;
|
|
4662
|
+
}
|
|
4663
|
+
function validateMeshSnapshot(data) {
|
|
4664
|
+
if (!isObject(data)) {
|
|
4665
|
+
throw new MeshSnapshotError("snapshot must be a JSON object");
|
|
4666
|
+
}
|
|
4667
|
+
if (typeof data["localPeerId"] !== "string") {
|
|
4668
|
+
throw new MeshSnapshotError("snapshot.localPeerId must be a string");
|
|
4669
|
+
}
|
|
4670
|
+
requireStringArray(data["knownPeerIds"], "snapshot.knownPeerIds");
|
|
4671
|
+
requireStringArray(data["presentPeerIds"], "snapshot.presentPeerIds");
|
|
4672
|
+
const peers = data["peers"];
|
|
4673
|
+
if (!Array.isArray(peers)) {
|
|
4674
|
+
throw new MeshSnapshotError("snapshot.peers must be an array");
|
|
4675
|
+
}
|
|
4676
|
+
peers.forEach((peer, i) => {
|
|
4677
|
+
validatePeer(peer, `snapshot.peers[${i}]`);
|
|
4678
|
+
});
|
|
4679
|
+
return data;
|
|
4680
|
+
}
|
|
4681
|
+
function loadMeshSnapshot(filePath) {
|
|
4682
|
+
let raw;
|
|
4683
|
+
try {
|
|
4684
|
+
raw = fs5.readFileSync(filePath, "utf-8");
|
|
4685
|
+
} catch (error) {
|
|
4686
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
4687
|
+
throw new MeshSnapshotError(`cannot read snapshot file '${filePath}': ${reason}`);
|
|
4688
|
+
}
|
|
4689
|
+
let parsed;
|
|
4690
|
+
try {
|
|
4691
|
+
parsed = JSON.parse(raw);
|
|
4692
|
+
} catch (error) {
|
|
4693
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
4694
|
+
throw new MeshSnapshotError(`snapshot file '${filePath}' is not valid JSON: ${reason}`);
|
|
4695
|
+
}
|
|
4696
|
+
return validateMeshSnapshot(parsed);
|
|
4697
|
+
}
|
|
4698
|
+
function collectSnapshotPeerIds(snapshot) {
|
|
4699
|
+
const ids = new Set([snapshot.localPeerId]);
|
|
4700
|
+
for (const id of snapshot.knownPeerIds)
|
|
4701
|
+
ids.add(id);
|
|
4702
|
+
for (const id of snapshot.presentPeerIds)
|
|
4703
|
+
ids.add(id);
|
|
4704
|
+
for (const peer of snapshot.peers)
|
|
4705
|
+
ids.add(peer.peerId);
|
|
4706
|
+
return [...ids];
|
|
4707
|
+
}
|
|
4708
|
+
|
|
4570
4709
|
// tools/visualize/src/types/structurizr.ts
|
|
4571
4710
|
var DEFAULT_COLORS = {
|
|
4572
4711
|
messageHandler: "#1168bd",
|
|
@@ -4639,6 +4778,11 @@ var DEFAULT_ELEMENT_STYLES = {
|
|
|
4639
4778
|
shape: "Cylinder",
|
|
4640
4779
|
background: DEFAULT_COLORS.queryHandler,
|
|
4641
4780
|
color: DEFAULT_COLORS.textDark
|
|
4781
|
+
},
|
|
4782
|
+
"Mesh Transport": {
|
|
4783
|
+
shape: "Pipe",
|
|
4784
|
+
background: DEFAULT_COLORS.service,
|
|
4785
|
+
color: DEFAULT_COLORS.textLight
|
|
4642
4786
|
}
|
|
4643
4787
|
};
|
|
4644
4788
|
var DEFAULT_RELATIONSHIP_STYLES = {
|
|
@@ -4662,12 +4806,83 @@ var DEFAULT_RELATIONSHIP_STYLES = {
|
|
|
4662
4806
|
color: DEFAULT_COLORS.database
|
|
4663
4807
|
}
|
|
4664
4808
|
};
|
|
4809
|
+
var MESH_OVERLAY_ELEMENT_STYLES = {
|
|
4810
|
+
"Mesh Peer": {
|
|
4811
|
+
shape: "Person",
|
|
4812
|
+
background: "#495057",
|
|
4813
|
+
color: DEFAULT_COLORS.textLight
|
|
4814
|
+
},
|
|
4815
|
+
"Local Mesh Peer": {
|
|
4816
|
+
shape: "Person",
|
|
4817
|
+
background: DEFAULT_COLORS.messageHandler,
|
|
4818
|
+
color: DEFAULT_COLORS.textLight
|
|
4819
|
+
},
|
|
4820
|
+
"Snapshot Document": {
|
|
4821
|
+
shape: "Cylinder",
|
|
4822
|
+
background: "#adb5bd",
|
|
4823
|
+
color: DEFAULT_COLORS.textDark,
|
|
4824
|
+
border: "Dashed"
|
|
4825
|
+
}
|
|
4826
|
+
};
|
|
4827
|
+
var MESH_OVERLAY_RELATIONSHIP_STYLES = {
|
|
4828
|
+
"sync:has": {
|
|
4829
|
+
color: "#2f9e44",
|
|
4830
|
+
style: "Solid",
|
|
4831
|
+
thickness: 3
|
|
4832
|
+
},
|
|
4833
|
+
"sync:wants": {
|
|
4834
|
+
color: "#f08c00",
|
|
4835
|
+
style: "Dashed",
|
|
4836
|
+
thickness: 2
|
|
4837
|
+
},
|
|
4838
|
+
"sync:unavailable": {
|
|
4839
|
+
color: "#e03131",
|
|
4840
|
+
style: "Dashed",
|
|
4841
|
+
thickness: 2
|
|
4842
|
+
},
|
|
4843
|
+
"sync:unknown": {
|
|
4844
|
+
color: "#868e96",
|
|
4845
|
+
style: "Dotted",
|
|
4846
|
+
thickness: 2
|
|
4847
|
+
}
|
|
4848
|
+
};
|
|
4665
4849
|
var DEFAULT_THEME = "https://static.structurizr.com/themes/default/theme.json";
|
|
4666
4850
|
|
|
4667
4851
|
// tools/visualize/src/codegen/structurizr.ts
|
|
4852
|
+
var MESH_TRANSPORT_NODES = [
|
|
4853
|
+
{
|
|
4854
|
+
id: "mesh_net_adapter",
|
|
4855
|
+
name: "MeshNetworkAdapter",
|
|
4856
|
+
description: "Signs and encrypts every mesh operation"
|
|
4857
|
+
},
|
|
4858
|
+
{
|
|
4859
|
+
id: "mesh_webrtc_adapter",
|
|
4860
|
+
name: "MeshWebRTCAdapter",
|
|
4861
|
+
description: "Peer-to-peer WebRTC data channels"
|
|
4862
|
+
},
|
|
4863
|
+
{
|
|
4864
|
+
id: "mesh_signaling_client",
|
|
4865
|
+
name: "MeshSignalingClient",
|
|
4866
|
+
description: "WebRTC offer/answer signalling"
|
|
4867
|
+
},
|
|
4868
|
+
{
|
|
4869
|
+
id: "mesh_signaling_endpoint",
|
|
4870
|
+
name: "Signalling endpoint",
|
|
4871
|
+
description: "The rendezvous server peers exchange WebRTC offers through"
|
|
4872
|
+
}
|
|
4873
|
+
];
|
|
4874
|
+
function syncStatusOf(handle) {
|
|
4875
|
+
const status = handle.peerDocumentStatus;
|
|
4876
|
+
if (status === "has" || status === "wants" || status === "unavailable") {
|
|
4877
|
+
return status;
|
|
4878
|
+
}
|
|
4879
|
+
return "unknown";
|
|
4880
|
+
}
|
|
4881
|
+
|
|
4668
4882
|
class StructurizrDSLGenerator {
|
|
4669
4883
|
analysis;
|
|
4670
4884
|
options;
|
|
4885
|
+
overlayPlanCache;
|
|
4671
4886
|
constructor(analysis, options = {}) {
|
|
4672
4887
|
this.analysis = analysis;
|
|
4673
4888
|
this.options = {
|
|
@@ -4686,10 +4901,12 @@ class StructurizrDSLGenerator {
|
|
|
4686
4901
|
theme: options.styles?.theme || DEFAULT_THEME,
|
|
4687
4902
|
elements: {
|
|
4688
4903
|
...DEFAULT_ELEMENT_STYLES,
|
|
4904
|
+
...options.snapshot ? MESH_OVERLAY_ELEMENT_STYLES : {},
|
|
4689
4905
|
...options.styles?.elements
|
|
4690
4906
|
},
|
|
4691
4907
|
relationships: {
|
|
4692
4908
|
...DEFAULT_RELATIONSHIP_STYLES,
|
|
4909
|
+
...options.snapshot ? MESH_OVERLAY_RELATIONSHIP_STYLES : {},
|
|
4693
4910
|
...options.styles?.relationships
|
|
4694
4911
|
}
|
|
4695
4912
|
}
|
|
@@ -4723,6 +4940,9 @@ class StructurizrDSLGenerator {
|
|
|
4723
4940
|
const parts = [];
|
|
4724
4941
|
parts.push(" model {");
|
|
4725
4942
|
parts.push(this.generatePeople());
|
|
4943
|
+
const meshPeers = this.generateMeshPeers();
|
|
4944
|
+
if (meshPeers)
|
|
4945
|
+
parts.push(meshPeers);
|
|
4726
4946
|
parts.push(this.generateExternalSystems());
|
|
4727
4947
|
parts.push(this.generateMainSystem());
|
|
4728
4948
|
if (this.options.deploymentNodes && this.options.deploymentNodes.length > 0) {
|
|
@@ -4763,6 +4983,12 @@ class StructurizrDSLGenerator {
|
|
|
4763
4983
|
const meshDocs = this.generateMeshDocuments();
|
|
4764
4984
|
if (meshDocs)
|
|
4765
4985
|
parts.push(meshDocs);
|
|
4986
|
+
const snapshotDocs = this.generateSnapshotOnlyDocuments();
|
|
4987
|
+
if (snapshotDocs)
|
|
4988
|
+
parts.push(snapshotDocs);
|
|
4989
|
+
const meshTransport = this.generateMeshTransport();
|
|
4990
|
+
if (meshTransport)
|
|
4991
|
+
parts.push(meshTransport);
|
|
4766
4992
|
parts.push(this.generateContainerRelationships());
|
|
4767
4993
|
parts.push(" }");
|
|
4768
4994
|
return parts.join(`
|
|
@@ -4794,7 +5020,8 @@ class StructurizrDSLGenerator {
|
|
|
4794
5020
|
continue;
|
|
4795
5021
|
seen.add(sig.key);
|
|
4796
5022
|
const kindLabel = sig.kind === "mesh" ? "Mesh Document" : "Peer Document";
|
|
4797
|
-
const
|
|
5023
|
+
const access = sig.access ? ` · access read=${this.clip(sig.access.read)} write=${this.clip(sig.access.write)}` : "";
|
|
5024
|
+
const description = `${kindLabel} — deriveDocumentId('${sig.key}')${access}`;
|
|
4798
5025
|
parts.push(` ${this.meshDocId(sig.key)} = container "${this.escape(sig.key)}" "${this.escape(description)}" "$${sig.kind}State" {`);
|
|
4799
5026
|
parts.push(` tags "${kindLabel}"`);
|
|
4800
5027
|
parts.push(" }");
|
|
@@ -4805,6 +5032,9 @@ class StructurizrDSLGenerator {
|
|
|
4805
5032
|
meshDocId(key) {
|
|
4806
5033
|
return `mesh_doc_${this.toId(key)}`;
|
|
4807
5034
|
}
|
|
5035
|
+
clip(text, max = 32) {
|
|
5036
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
5037
|
+
}
|
|
4808
5038
|
contextForFilePath(filePath) {
|
|
4809
5039
|
const contextKeys = Object.keys(this.analysis.contexts);
|
|
4810
5040
|
if (contextKeys.length === 1)
|
|
@@ -5143,6 +5373,8 @@ class StructurizrDSLGenerator {
|
|
|
5143
5373
|
parts.push(...this.generateMessageFlowRelationships());
|
|
5144
5374
|
parts.push(...this.generateExternalAPIRelationships());
|
|
5145
5375
|
parts.push(...this.generateMeshRelationships());
|
|
5376
|
+
parts.push(...this.generateMeshTransportRelationships());
|
|
5377
|
+
parts.push(...this.generateMeshOverlayRelationships());
|
|
5146
5378
|
return parts.join(`
|
|
5147
5379
|
`);
|
|
5148
5380
|
}
|
|
@@ -5162,6 +5394,177 @@ class StructurizrDSLGenerator {
|
|
|
5162
5394
|
}
|
|
5163
5395
|
return parts;
|
|
5164
5396
|
}
|
|
5397
|
+
meshSignalKinds() {
|
|
5398
|
+
const signals = this.analysis.meshOrPeerSignals ?? [];
|
|
5399
|
+
return {
|
|
5400
|
+
mesh: signals.some((s) => s.kind === "mesh"),
|
|
5401
|
+
peer: signals.some((s) => s.kind === "peer")
|
|
5402
|
+
};
|
|
5403
|
+
}
|
|
5404
|
+
generateMeshTransport() {
|
|
5405
|
+
const { mesh, peer } = this.meshSignalKinds();
|
|
5406
|
+
if (!mesh && !peer)
|
|
5407
|
+
return "";
|
|
5408
|
+
const parts = [];
|
|
5409
|
+
if (mesh) {
|
|
5410
|
+
for (const node of MESH_TRANSPORT_NODES) {
|
|
5411
|
+
parts.push(` ${node.id} = container "${node.name}" "${this.escape(node.description)}" "Mesh Transport" {`);
|
|
5412
|
+
parts.push(' tags "Mesh Transport"');
|
|
5413
|
+
parts.push(" }");
|
|
5414
|
+
}
|
|
5415
|
+
}
|
|
5416
|
+
if (peer) {
|
|
5417
|
+
parts.push(` peer_relay = container "Peer relay" "${this.escape("The always-on relay $peerState syncs through")}" "Peer Transport" {`);
|
|
5418
|
+
parts.push(' tags "Mesh Transport"');
|
|
5419
|
+
parts.push(" }");
|
|
5420
|
+
}
|
|
5421
|
+
return parts.join(`
|
|
5422
|
+
`);
|
|
5423
|
+
}
|
|
5424
|
+
generateMeshTransportRelationships() {
|
|
5425
|
+
const { mesh, peer } = this.meshSignalKinds();
|
|
5426
|
+
const parts = [];
|
|
5427
|
+
if (mesh) {
|
|
5428
|
+
parts.push(' extension.mesh_net_adapter -> extension.mesh_webrtc_adapter "wraps"');
|
|
5429
|
+
parts.push(' extension.mesh_webrtc_adapter -> extension.mesh_signaling_client "negotiates via"');
|
|
5430
|
+
parts.push(' extension.mesh_signaling_client -> extension.mesh_signaling_endpoint "connects to"');
|
|
5431
|
+
}
|
|
5432
|
+
if (!mesh && !peer)
|
|
5433
|
+
return parts;
|
|
5434
|
+
const seen = new Set;
|
|
5435
|
+
for (const sig of this.analysis.meshOrPeerSignals ?? []) {
|
|
5436
|
+
if (seen.has(sig.key))
|
|
5437
|
+
continue;
|
|
5438
|
+
seen.add(sig.key);
|
|
5439
|
+
const target = sig.kind === "mesh" ? "mesh_net_adapter" : "peer_relay";
|
|
5440
|
+
parts.push(` extension.${this.meshDocId(sig.key)} -> extension.${target} "syncs through"`);
|
|
5441
|
+
}
|
|
5442
|
+
return parts;
|
|
5443
|
+
}
|
|
5444
|
+
getOverlayPlan() {
|
|
5445
|
+
const snapshot = this.options.snapshot;
|
|
5446
|
+
if (!snapshot)
|
|
5447
|
+
return;
|
|
5448
|
+
if (!this.overlayPlanCache) {
|
|
5449
|
+
this.overlayPlanCache = this.buildOverlayPlan(snapshot);
|
|
5450
|
+
}
|
|
5451
|
+
return this.overlayPlanCache;
|
|
5452
|
+
}
|
|
5453
|
+
buildOverlayPlan(snapshot) {
|
|
5454
|
+
const docIdToNode = this.buildDocIdNodeMap();
|
|
5455
|
+
const { peers, peerDslById } = this.buildOverlayPeers(snapshot);
|
|
5456
|
+
const { edges, snapshotOnlyDocs } = this.buildOverlayEdges(snapshot, docIdToNode, peerDslById);
|
|
5457
|
+
return { peers, edges, snapshotOnlyDocs };
|
|
5458
|
+
}
|
|
5459
|
+
buildDocIdNodeMap() {
|
|
5460
|
+
const docIdToNode = new Map;
|
|
5461
|
+
const seenKeys = new Set;
|
|
5462
|
+
for (const sig of this.analysis.meshOrPeerSignals ?? []) {
|
|
5463
|
+
if (seenKeys.has(sig.key))
|
|
5464
|
+
continue;
|
|
5465
|
+
seenKeys.add(sig.key);
|
|
5466
|
+
docIdToNode.set(String(deriveDocumentId(sig.key)), this.meshDocId(sig.key));
|
|
5467
|
+
}
|
|
5468
|
+
return docIdToNode;
|
|
5469
|
+
}
|
|
5470
|
+
buildOverlayPeers(snapshot) {
|
|
5471
|
+
const peers = [];
|
|
5472
|
+
const peerDslById = new Map;
|
|
5473
|
+
const usedPeerIds = new Set;
|
|
5474
|
+
for (const peerId of collectSnapshotPeerIds(snapshot)) {
|
|
5475
|
+
const base = `peer_${this.toId(peerId)}`;
|
|
5476
|
+
let id = base;
|
|
5477
|
+
let suffix = 2;
|
|
5478
|
+
while (usedPeerIds.has(id))
|
|
5479
|
+
id = `${base}_${suffix++}`;
|
|
5480
|
+
usedPeerIds.add(id);
|
|
5481
|
+
peerDslById.set(peerId, id);
|
|
5482
|
+
peers.push({ id, peerId, isLocal: peerId === snapshot.localPeerId });
|
|
5483
|
+
}
|
|
5484
|
+
return { peers, peerDslById };
|
|
5485
|
+
}
|
|
5486
|
+
buildOverlayEdges(snapshot, docIdToNode, peerDslById) {
|
|
5487
|
+
const edges = [];
|
|
5488
|
+
const snapshotOnlyDocs = [];
|
|
5489
|
+
const snapshotOnlyById = new Map;
|
|
5490
|
+
for (const peer of snapshot.peers) {
|
|
5491
|
+
const handles = peer.slot?.handles;
|
|
5492
|
+
const fromId = peerDslById.get(peer.peerId);
|
|
5493
|
+
if (!handles || !fromId)
|
|
5494
|
+
continue;
|
|
5495
|
+
for (const [docId, handle] of Object.entries(handles)) {
|
|
5496
|
+
const nodeId = this.resolveOverlayDocNode(docId, docIdToNode, snapshotOnlyById, snapshotOnlyDocs);
|
|
5497
|
+
const status = syncStatusOf(handle);
|
|
5498
|
+
edges.push({
|
|
5499
|
+
fromId,
|
|
5500
|
+
toRef: `extension.${nodeId}`,
|
|
5501
|
+
status,
|
|
5502
|
+
label: this.overlayEdgeLabel(status, handle)
|
|
5503
|
+
});
|
|
5504
|
+
}
|
|
5505
|
+
}
|
|
5506
|
+
return { edges, snapshotOnlyDocs };
|
|
5507
|
+
}
|
|
5508
|
+
resolveOverlayDocNode(docId, docIdToNode, snapshotOnlyById, snapshotOnlyDocs) {
|
|
5509
|
+
const staticNode = docIdToNode.get(docId);
|
|
5510
|
+
if (staticNode)
|
|
5511
|
+
return staticNode;
|
|
5512
|
+
const existing = snapshotOnlyById.get(docId);
|
|
5513
|
+
if (existing)
|
|
5514
|
+
return existing;
|
|
5515
|
+
const nodeId = `mesh_doc_${this.toId(docId)}`;
|
|
5516
|
+
snapshotOnlyById.set(docId, nodeId);
|
|
5517
|
+
snapshotOnlyDocs.push({ id: nodeId, docId });
|
|
5518
|
+
return nodeId;
|
|
5519
|
+
}
|
|
5520
|
+
overlayEdgeLabel(status, handle) {
|
|
5521
|
+
if (!handle.docSynchronizerExists)
|
|
5522
|
+
return `${status} · no synchronizer`;
|
|
5523
|
+
if (handle.docSynchronizerKnowsPeer === false)
|
|
5524
|
+
return `${status} · peer not added`;
|
|
5525
|
+
return status;
|
|
5526
|
+
}
|
|
5527
|
+
generateMeshPeers() {
|
|
5528
|
+
const plan = this.getOverlayPlan();
|
|
5529
|
+
if (!plan || plan.peers.length === 0)
|
|
5530
|
+
return "";
|
|
5531
|
+
const parts = [];
|
|
5532
|
+
for (const peer of plan.peers) {
|
|
5533
|
+
const tag = peer.isLocal ? "Local Mesh Peer" : "Mesh Peer";
|
|
5534
|
+
const description = peer.isLocal ? "Local mesh peer (this node)" : "Runtime mesh peer";
|
|
5535
|
+
parts.push(` ${peer.id} = person "${this.escape(peer.peerId)}" "${description}" {`);
|
|
5536
|
+
parts.push(` tags "${tag}"`);
|
|
5537
|
+
parts.push(" }");
|
|
5538
|
+
}
|
|
5539
|
+
return parts.join(`
|
|
5540
|
+
`);
|
|
5541
|
+
}
|
|
5542
|
+
generateSnapshotOnlyDocuments() {
|
|
5543
|
+
const plan = this.getOverlayPlan();
|
|
5544
|
+
if (!plan || plan.snapshotOnlyDocs.length === 0)
|
|
5545
|
+
return "";
|
|
5546
|
+
const parts = [];
|
|
5547
|
+
for (const doc of plan.snapshotOnlyDocs) {
|
|
5548
|
+
const description = `Mesh document seen only in the runtime snapshot — docId ${doc.docId}`;
|
|
5549
|
+
parts.push(` ${doc.id} = container "${this.escape(doc.docId)}" "${this.escape(description)}" "snapshot" {`);
|
|
5550
|
+
parts.push(' tags "Snapshot Document"');
|
|
5551
|
+
parts.push(" }");
|
|
5552
|
+
}
|
|
5553
|
+
return parts.join(`
|
|
5554
|
+
`);
|
|
5555
|
+
}
|
|
5556
|
+
generateMeshOverlayRelationships() {
|
|
5557
|
+
const plan = this.getOverlayPlan();
|
|
5558
|
+
if (!plan)
|
|
5559
|
+
return [];
|
|
5560
|
+
const parts = [];
|
|
5561
|
+
for (const edge of plan.edges) {
|
|
5562
|
+
parts.push(` ${edge.fromId} -> ${edge.toRef} "${this.escape(edge.label)}" {`);
|
|
5563
|
+
parts.push(` tags "sync:${edge.status}"`);
|
|
5564
|
+
parts.push(" }");
|
|
5565
|
+
}
|
|
5566
|
+
return parts;
|
|
5567
|
+
}
|
|
5165
5568
|
generateUserRelationships() {
|
|
5166
5569
|
const parts = [];
|
|
5167
5570
|
const uiContexts = ["popup", "options", "devtools"];
|
|
@@ -5909,7 +6312,7 @@ function generateStructurizrDSL(analysis, options) {
|
|
|
5909
6312
|
|
|
5910
6313
|
// tools/visualize/src/runner/export.ts
|
|
5911
6314
|
import { spawn } from "node:child_process";
|
|
5912
|
-
import * as
|
|
6315
|
+
import * as fs6 from "node:fs";
|
|
5913
6316
|
import * as path5 from "node:path";
|
|
5914
6317
|
|
|
5915
6318
|
class DiagramExporter {
|
|
@@ -5917,15 +6320,15 @@ class DiagramExporter {
|
|
|
5917
6320
|
static DEFAULT_TIMEOUT = 120000;
|
|
5918
6321
|
async export(options) {
|
|
5919
6322
|
const { dslPath, outputDir, timeout = DiagramExporter.DEFAULT_TIMEOUT } = options;
|
|
5920
|
-
if (!
|
|
6323
|
+
if (!fs6.existsSync(dslPath)) {
|
|
5921
6324
|
return {
|
|
5922
6325
|
success: false,
|
|
5923
6326
|
siteDir: "",
|
|
5924
6327
|
error: `DSL file not found: ${dslPath}`
|
|
5925
6328
|
};
|
|
5926
6329
|
}
|
|
5927
|
-
if (!
|
|
5928
|
-
|
|
6330
|
+
if (!fs6.existsSync(outputDir)) {
|
|
6331
|
+
fs6.mkdirSync(outputDir, { recursive: true });
|
|
5929
6332
|
}
|
|
5930
6333
|
const dockerAvailable = await this.isDockerAvailable();
|
|
5931
6334
|
if (!dockerAvailable) {
|
|
@@ -6073,7 +6476,7 @@ async function main() {
|
|
|
6073
6476
|
switch (command) {
|
|
6074
6477
|
case "--generate":
|
|
6075
6478
|
case "generate":
|
|
6076
|
-
await generateCommand();
|
|
6479
|
+
await generateCommand(args.slice(1));
|
|
6077
6480
|
break;
|
|
6078
6481
|
case "--export":
|
|
6079
6482
|
case "export":
|
|
@@ -6090,22 +6493,64 @@ async function main() {
|
|
|
6090
6493
|
showHelp();
|
|
6091
6494
|
break;
|
|
6092
6495
|
default:
|
|
6093
|
-
await generateCommand();
|
|
6496
|
+
await generateCommand(args);
|
|
6094
6497
|
}
|
|
6095
6498
|
}
|
|
6096
|
-
async function generateCommand() {
|
|
6499
|
+
async function generateCommand(args) {
|
|
6097
6500
|
console.log(color(`
|
|
6098
6501
|
\uD83D\uDCCA Analyzing architecture...
|
|
6099
6502
|
`, COLORS.blue));
|
|
6100
6503
|
try {
|
|
6504
|
+
const { snapshotPath } = parseGenerateArgs(args);
|
|
6505
|
+
const snapshot = snapshotPath ? loadAndReportSnapshot(snapshotPath) : undefined;
|
|
6101
6506
|
const { tsConfigPath, projectRoot } = findAndDisplayProjectConfig();
|
|
6102
6507
|
const analysis = await analyzeAndDisplayResults(tsConfigPath, projectRoot);
|
|
6103
|
-
const dslPath = generateAndWriteDSL(analysis);
|
|
6508
|
+
const dslPath = generateAndWriteDSL(analysis, snapshot);
|
|
6104
6509
|
displayNextSteps(dslPath);
|
|
6105
6510
|
} catch (_error) {
|
|
6106
6511
|
process.exit(1);
|
|
6107
6512
|
}
|
|
6108
6513
|
}
|
|
6514
|
+
function parseGenerateArgs(args) {
|
|
6515
|
+
const result = {};
|
|
6516
|
+
for (let i = 0;i < args.length; i++) {
|
|
6517
|
+
const arg = args[i];
|
|
6518
|
+
if (arg === "--snapshot") {
|
|
6519
|
+
const next = args[i + 1];
|
|
6520
|
+
if (!next || next.startsWith("--")) {
|
|
6521
|
+
console.log(color(`
|
|
6522
|
+
✗ --snapshot requires a file path
|
|
6523
|
+
`, COLORS.red));
|
|
6524
|
+
process.exit(1);
|
|
6525
|
+
}
|
|
6526
|
+
result.snapshotPath = next;
|
|
6527
|
+
i++;
|
|
6528
|
+
} else if (arg?.startsWith("--snapshot=")) {
|
|
6529
|
+
result.snapshotPath = arg.slice("--snapshot=".length);
|
|
6530
|
+
}
|
|
6531
|
+
}
|
|
6532
|
+
return result;
|
|
6533
|
+
}
|
|
6534
|
+
function loadAndReportSnapshot(snapshotPath) {
|
|
6535
|
+
let snapshot;
|
|
6536
|
+
try {
|
|
6537
|
+
snapshot = loadMeshSnapshot(snapshotPath);
|
|
6538
|
+
} catch (error) {
|
|
6539
|
+
const message = error instanceof MeshSnapshotError ? error.message : String(error);
|
|
6540
|
+
console.log(color(`
|
|
6541
|
+
✗ Snapshot error: ${message}
|
|
6542
|
+
`, COLORS.red));
|
|
6543
|
+
process.exit(1);
|
|
6544
|
+
}
|
|
6545
|
+
const isEmpty = snapshot.knownPeerIds.length === 0 && snapshot.presentPeerIds.length === 0 && snapshot.peers.length === 0;
|
|
6546
|
+
if (isEmpty) {
|
|
6547
|
+
console.log(color("⚠ Snapshot has no peers — generating the static diagram only.", COLORS.yellow));
|
|
6548
|
+
return;
|
|
6549
|
+
}
|
|
6550
|
+
const peerCount = collectSnapshotPeerIds(snapshot).length;
|
|
6551
|
+
console.log(color(`✓ Loaded runtime snapshot: ${peerCount} peer(s)`, COLORS.green));
|
|
6552
|
+
return snapshot;
|
|
6553
|
+
}
|
|
6109
6554
|
function findAndDisplayProjectConfig() {
|
|
6110
6555
|
const tsConfigPath = findTsConfig();
|
|
6111
6556
|
if (!tsConfigPath) {
|
|
@@ -6121,7 +6566,7 @@ function findAndDisplayProjectConfig() {
|
|
|
6121
6566
|
return { tsConfigPath, projectRoot };
|
|
6122
6567
|
}
|
|
6123
6568
|
function displayProjectType(projectRoot) {
|
|
6124
|
-
const hasManifest =
|
|
6569
|
+
const hasManifest = fs7.existsSync(path6.join(projectRoot, "manifest.json"));
|
|
6125
6570
|
const projectType = hasManifest ? "Chrome Extension" : "Detecting from project structure...";
|
|
6126
6571
|
console.log(color(` Type: ${projectType}`, COLORS.gray));
|
|
6127
6572
|
}
|
|
@@ -6155,7 +6600,7 @@ function displayArchitectureSummary(analysis) {
|
|
|
6155
6600
|
}
|
|
6156
6601
|
}
|
|
6157
6602
|
}
|
|
6158
|
-
function generateAndWriteDSL(analysis) {
|
|
6603
|
+
function generateAndWriteDSL(analysis, snapshot) {
|
|
6159
6604
|
console.log(color(`
|
|
6160
6605
|
\uD83D\uDCDD Generating Structurizr DSL...
|
|
6161
6606
|
`, COLORS.blue));
|
|
@@ -6163,14 +6608,15 @@ function generateAndWriteDSL(analysis) {
|
|
|
6163
6608
|
const dsl = generateStructurizrDSL(analysis, {
|
|
6164
6609
|
includeDynamicDiagrams: true,
|
|
6165
6610
|
includeComponentDiagrams: true,
|
|
6166
|
-
componentDiagramContexts: contextTypes.length > 0 ? contextTypes : ["background"]
|
|
6611
|
+
componentDiagramContexts: contextTypes.length > 0 ? contextTypes : ["background"],
|
|
6612
|
+
snapshot
|
|
6167
6613
|
});
|
|
6168
6614
|
const outputDir = path6.join(process.cwd(), "docs");
|
|
6169
|
-
if (!
|
|
6170
|
-
|
|
6615
|
+
if (!fs7.existsSync(outputDir)) {
|
|
6616
|
+
fs7.mkdirSync(outputDir, { recursive: true });
|
|
6171
6617
|
}
|
|
6172
6618
|
const dslPath = path6.join(outputDir, "architecture.dsl");
|
|
6173
|
-
|
|
6619
|
+
fs7.writeFileSync(dslPath, dsl, "utf-8");
|
|
6174
6620
|
return dslPath;
|
|
6175
6621
|
}
|
|
6176
6622
|
function displayNextSteps(dslPath) {
|
|
@@ -6206,7 +6652,7 @@ async function exportCommand(_args) {
|
|
|
6206
6652
|
`, COLORS.blue));
|
|
6207
6653
|
try {
|
|
6208
6654
|
const dslPath = path6.join(process.cwd(), "docs", "architecture.dsl");
|
|
6209
|
-
if (!
|
|
6655
|
+
if (!fs7.existsSync(dslPath)) {
|
|
6210
6656
|
process.exit(1);
|
|
6211
6657
|
}
|
|
6212
6658
|
const outputDir = path6.join(process.cwd(), "docs", "site");
|
|
@@ -6244,7 +6690,7 @@ async function serveCommand(args) {
|
|
|
6244
6690
|
try {
|
|
6245
6691
|
const siteDir = path6.join(process.cwd(), "docs", "site");
|
|
6246
6692
|
const indexPath = path6.join(siteDir, "index.html");
|
|
6247
|
-
if (!
|
|
6693
|
+
if (!fs7.existsSync(indexPath)) {
|
|
6248
6694
|
process.exit(1);
|
|
6249
6695
|
}
|
|
6250
6696
|
const portArg = args.find((arg) => arg.startsWith("--port="));
|
|
@@ -6261,7 +6707,7 @@ async function serveCommand(args) {
|
|
|
6261
6707
|
fetch(req) {
|
|
6262
6708
|
const url = new URL(req.url);
|
|
6263
6709
|
const filePath = path6.join(siteDir, url.pathname === "/" ? "index.html" : url.pathname);
|
|
6264
|
-
if (
|
|
6710
|
+
if (fs7.existsSync(filePath) && fs7.statSync(filePath).isFile()) {
|
|
6265
6711
|
const file = BunGlobal.file(filePath);
|
|
6266
6712
|
return new Response(file);
|
|
6267
6713
|
}
|
|
@@ -6305,6 +6751,10 @@ ${color("Commands:", COLORS.blue)}
|
|
|
6305
6751
|
${color("bun visualize --generate", COLORS.green)}
|
|
6306
6752
|
Analyze codebase and generate Structurizr DSL
|
|
6307
6753
|
|
|
6754
|
+
${color("bun visualize generate --snapshot <path>", COLORS.green)}
|
|
6755
|
+
Overlay a captured MeshClientPeerStateSnapshot — runtime mesh peers
|
|
6756
|
+
and sync-state-coloured replication edges — onto the diagram
|
|
6757
|
+
|
|
6308
6758
|
${color("bun visualize --export", COLORS.green)}
|
|
6309
6759
|
Generate static HTML site with interactive diagrams (requires Docker)
|
|
6310
6760
|
|
|
@@ -6341,7 +6791,7 @@ function findTsConfig() {
|
|
|
6341
6791
|
path6.join(process.cwd(), "..", "tsconfig.json")
|
|
6342
6792
|
];
|
|
6343
6793
|
for (const loc of locations) {
|
|
6344
|
-
if (
|
|
6794
|
+
if (fs7.existsSync(loc)) {
|
|
6345
6795
|
return loc;
|
|
6346
6796
|
}
|
|
6347
6797
|
}
|
|
@@ -6350,7 +6800,7 @@ function findTsConfig() {
|
|
|
6350
6800
|
function findProjectRoot() {
|
|
6351
6801
|
const locations = [process.cwd(), path6.join(process.cwd(), "..")];
|
|
6352
6802
|
for (const loc of locations) {
|
|
6353
|
-
if (
|
|
6803
|
+
if (fs7.existsSync(path6.join(loc, "manifest.json")) || fs7.existsSync(path6.join(loc, "package.json")) || fs7.existsSync(path6.join(loc, "tsconfig.json"))) {
|
|
6354
6804
|
return loc;
|
|
6355
6805
|
}
|
|
6356
6806
|
}
|
|
@@ -6360,4 +6810,4 @@ main().catch((_error) => {
|
|
|
6360
6810
|
process.exit(1);
|
|
6361
6811
|
});
|
|
6362
6812
|
|
|
6363
|
-
//# debugId=
|
|
6813
|
+
//# debugId=4CFC761842CDF97B64756E2164756E21
|