├── pnpm-workspace.yaml ├── test ├── __image_snapshots__ │ ├── index-test-ts-render-all-fields-png-1-snap.png │ ├── index-test-ts-render-workers-aws-png-1-snap.png │ ├── index-test-ts-render-web-services-aws-png-1-snap.png │ ├── index-test-ts-render-events-processing-aws-png-1-snap.png │ ├── index-test-ts-render-message-collecting-gcp-png-1-snap.png │ ├── index-test-ts-render-exposed-pods-kubernetes-png-1-snap.png │ └── index-test-ts-render-web-services-on-premise-png-1-snap.png ├── yaml │ ├── workers-aws.yaml │ ├── exposed-pods-kubernetes.yaml │ ├── all-fields.yaml │ ├── web-services-aws.yaml │ ├── events-processing-aws.yaml │ ├── message-collecting-gcp.yaml │ └── web-services-on-premise.yaml ├── index.test.ts └── svg │ ├── all-fields.svg │ ├── exposed-pods-kubernetes.svg │ ├── workers-aws.svg │ ├── web-services-aws.svg │ ├── events-processing-aws.svg │ ├── message-collecting-gcp.svg │ └── web-services-on-premise.svg ├── src ├── index.ts ├── shared.ts ├── v1.ts ├── v2.ts └── aliases.ts ├── experiment ├── index.ts ├── README.md └── ilograph.ts ├── tsconfig.json ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── README.md ├── package.json ├── .gitignore └── pnpm-lock.yaml /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | onlyBuiltDependencies: 2 | - esbuild 3 | -------------------------------------------------------------------------------- /test/__image_snapshots__/index-test-ts-render-all-fields-png-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stereobooster/diagrams-as-code/main/test/__image_snapshots__/index-test-ts-render-all-fields-png-1-snap.png -------------------------------------------------------------------------------- /test/__image_snapshots__/index-test-ts-render-workers-aws-png-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stereobooster/diagrams-as-code/main/test/__image_snapshots__/index-test-ts-render-workers-aws-png-1-snap.png -------------------------------------------------------------------------------- /test/__image_snapshots__/index-test-ts-render-web-services-aws-png-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stereobooster/diagrams-as-code/main/test/__image_snapshots__/index-test-ts-render-web-services-aws-png-1-snap.png -------------------------------------------------------------------------------- /test/__image_snapshots__/index-test-ts-render-events-processing-aws-png-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stereobooster/diagrams-as-code/main/test/__image_snapshots__/index-test-ts-render-events-processing-aws-png-1-snap.png -------------------------------------------------------------------------------- /test/__image_snapshots__/index-test-ts-render-message-collecting-gcp-png-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stereobooster/diagrams-as-code/main/test/__image_snapshots__/index-test-ts-render-message-collecting-gcp-png-1-snap.png -------------------------------------------------------------------------------- /test/__image_snapshots__/index-test-ts-render-exposed-pods-kubernetes-png-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stereobooster/diagrams-as-code/main/test/__image_snapshots__/index-test-ts-render-exposed-pods-kubernetes-png-1-snap.png -------------------------------------------------------------------------------- /test/__image_snapshots__/index-test-ts-render-web-services-on-premise-png-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stereobooster/diagrams-as-code/main/test/__image_snapshots__/index-test-ts-render-web-services-on-premise-png-1-snap.png -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { diagramSchema } from "./schema.js"; 2 | export { parse } from "./shared.js"; 3 | // export { toDot, render } from "./v1.js"; 4 | 5 | import { Format } from "@hpcc-js/wasm-graphviz"; 6 | import { RenderGraphviz } from "./v2.js"; 7 | export function render(file: string, format?: Format) { 8 | return new RenderGraphviz(file).render(format); 9 | } 10 | -------------------------------------------------------------------------------- /experiment/index.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "node:fs"; 2 | import { parse as parseYaml } from "yaml"; 3 | import { ilographSchema } from "./ilograph"; 4 | 5 | try { 6 | const filePath = "experiment/example/purchase-subscription.yaml"; 7 | const file = readFileSync(filePath, "utf8"); 8 | const rawData = parseYaml(file) as any; 9 | const data = ilographSchema.parse(rawData); 10 | 11 | console.log(data.resources); 12 | // console.log(data.perspectives?.[0]); 13 | } catch (e) { 14 | console.log(e.format()); 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "target": "ES2022", 5 | "module": "node16", 6 | "lib": ["ES2022"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "Node16", 11 | "esModuleInterop": true, 12 | "declaration": true, 13 | "noEmit": false, 14 | "outDir": "./dist", 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | "types": ["node"] 23 | }, 24 | "include": ["./src"], 25 | "rootDir": "./src" 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | types: [opened, synchronize] 8 | 9 | jobs: 10 | build: 11 | name: Test 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Check out code 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 2 19 | 20 | - uses: pnpm/action-setup@v3 21 | with: 22 | version: 10 23 | run_install: false 24 | 25 | - uses: actions/setup-node@v4 26 | with: 27 | # node-version-file: ".node-version" 28 | cache: "pnpm" 29 | 30 | - name: Install dependencies 31 | run: pnpm install 32 | 33 | - name: Test 34 | run: pnpm test 35 | 36 | - name: Build 37 | run: pnpm build 38 | -------------------------------------------------------------------------------- /test/yaml/workers-aws.yaml: -------------------------------------------------------------------------------- 1 | diagram: 2 | name: Workers Architecture on AWS 3 | direction: top-to-bottom 4 | open: true 5 | resources: 6 | - id: elb 7 | name: ELB 8 | type: aws.network.ELB 9 | relates: 10 | - to: workers 11 | direction: outgoing 12 | - id: workers 13 | name: Workers 14 | type: group 15 | of: 16 | - id: first-worker 17 | name: Worker №1 18 | type: aws.compute.EC2 19 | - id: second-worker 20 | name: Worker №2 21 | type: aws.compute.EC2 22 | - id: third-worker 23 | name: Worker №3 24 | type: aws.compute.EC2 25 | - id: fourth-worker 26 | name: Worker №4 27 | type: aws.compute.EC2 28 | - id: fifth-worker 29 | name: Worker №5 30 | type: aws.compute.EC2 31 | - id: sixth-worker 32 | name: Worker №6 33 | type: aws.compute.EC2 34 | relates: 35 | - to: database 36 | direction: outgoing 37 | - id: database 38 | name: Events 39 | type: aws.database.RDS 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 stereobooster 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # diagrams-as-code 2 | 3 | Port of https://github.com/dmytrostriletskyi/diagrams-as-code 4 | 5 | ## Installation 6 | 7 | ``` 8 | pnpm add diagrams-as-code 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```ts 14 | import { readFileSync } from "node:fs"; 15 | import { render } from "diagrams-as-code"; 16 | 17 | const filePath = "./examples/message-collecting-gcp.yaml"; 18 | console.log(render(readFileSync(filePath, "utf8"))); 19 | ``` 20 | 21 | ## TODO 22 | 23 | - [ ] implement [c4](https://docs.structurizr.com/dsl/language) 24 | - [ ] implement [ilograph](https://www.ilograph.com/docs/spec/) 25 | - [ ] try [elk](https://github.com/kieler/elkjs) instead of Graphviz 26 | - [ ] online demo, like [this one](https://powderizer.stereobooster.com/) 27 | - [ ] embed SVG files instead of using urls (otherwise images can be blocked by Content Security Policy) 28 | - [postprocess SVG](https://softwarerecs.stackexchange.com/questions/76954/how-can-i-convert-an-svg-with-linked-images-to-embed-those-images-inside-the-svg) 29 | - use package with icons, like [aws-svg-icons](https://www.npmjs.com/package/aws-svg-icons) 30 | - [ ] don't use `raw.githubusercontent.com` 31 | -------------------------------------------------------------------------------- /test/yaml/exposed-pods-kubernetes.yaml: -------------------------------------------------------------------------------- 1 | diagram: 2 | name: Exposed Pods Architecture on Kubernetes 3 | open: true 4 | resources: 5 | - id: ingress 6 | name: Ingress 7 | type: k8s.network.Ingress 8 | relates: 9 | - to: service 10 | direction: outgoing 11 | - id: service 12 | name: Service 13 | type: k8s.network.Service 14 | relates: 15 | - to: pods 16 | direction: outgoing 17 | - id: pods 18 | name: Pods 19 | type: group 20 | of: 21 | - id: first-pod 22 | name: Pod №1 23 | type: k8s.compute.Pod 24 | - id: second-pod 25 | name: Pod №2 26 | type: k8s.compute.Pod 27 | - id: third-pod 28 | name: Pod №3 29 | type: k8s.compute.Pod 30 | relates: 31 | - to: replica-set 32 | direction: incoming 33 | - id: replica-set 34 | name: Replica Set 35 | type: k8s.compute.ReplicaSet 36 | relates: 37 | - to: deployment 38 | direction: incoming 39 | - id: deployment 40 | name: Deployment 41 | type: k8s.compute.Deployment 42 | relates: 43 | - to: hpa 44 | direction: incoming 45 | - id: hpa 46 | name: HPA 47 | type: k8s.clusterconfig.HPA 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "diagrams-as-code", 3 | "version": "0.0.1", 4 | "description": "Port of https://github.com/dmytrostriletskyi/diagrams-as-code", 5 | "type": "module", 6 | "keywords": [ 7 | "yaml", 8 | "graphviz", 9 | "architecture", 10 | "diagrams", 11 | "system-design", 12 | "diagrams-as-code" 13 | ], 14 | "author": "stereobooster", 15 | "license": "MIT", 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/stereobooster/diagrams-as-code.git" 19 | }, 20 | "sideEffects": false, 21 | "exports": { 22 | ".": { 23 | "types": "./dist/index.d.ts", 24 | "import": "./dist/index.js" 25 | } 26 | }, 27 | "main": "./dist/index.js", 28 | "module": "./dist/index.js", 29 | "files": [ 30 | "dist" 31 | ], 32 | "types": "./dist/index.d.ts", 33 | "scripts": { 34 | "test": "vitest", 35 | "build": "rm -rf dist && tsc", 36 | "dev": "tsc --watch", 37 | "clean": "rm -rf dist" 38 | }, 39 | "packageManager": "pnpm@10.9.0", 40 | "dependencies": { 41 | "@hpcc-js/wasm-graphviz": "^1.7.0", 42 | "mnemonist": "^0.40.3", 43 | "ts-graphviz": "^2.1.6", 44 | "yaml": "^2.7.1", 45 | "zod": "3.24.1" 46 | }, 47 | "devDependencies": { 48 | "@resvg/resvg-js": "^2.6.2", 49 | "@types/node": "^22.14.1", 50 | "jest-image-snapshot": "^6.4.0", 51 | "typescript": "^5.8.3", 52 | "vitest": "^3.1.2", 53 | "zod-to-json-schema": "^3.24.5" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/yaml/all-fields.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/dmytrostriletskyi/diagrams-as-code/main/json-schemas/0.0.1.json 2 | diagram: 3 | name: Web Services Architecture on AWS 4 | file_name: web-services-architecture-aws 5 | format: jpg 6 | direction: left-to-right 7 | style: 8 | graph: 9 | splines: ortho 10 | node: 11 | shape: circle 12 | edge: 13 | color: "#000000" 14 | label_resources: false 15 | open: true 16 | resources: 17 | - id: dns 18 | name: DNS 19 | type: aws.network.Route53 20 | relates: 21 | - to: elb 22 | direction: outgoing 23 | label: Makes Request 24 | color: brown 25 | style: dotted 26 | - id: elb 27 | name: ELB 28 | type: aws.network.ELB 29 | relates: 30 | - to: web-services.graphql-api 31 | direction: outgoing 32 | label: Proxy Request 33 | color: firebrick 34 | style: dashed 35 | - id: web-services 36 | name: Web Services 37 | type: cluster 38 | of: 39 | - id: graphql-api 40 | name: GraphQL API 41 | type: group 42 | of: 43 | - id: first-ecs 44 | name: GraphQL API №1 45 | type: aws.compute.ECS 46 | - id: second-ecs 47 | name: GraphQL API №2 48 | type: aws.compute.ECS 49 | relates: 50 | - to: databases.leader 51 | direction: outgoing 52 | - id: databases 53 | name: Databases 54 | type: cluster 55 | of: 56 | - id: leader 57 | name: R/W Leader 58 | type: aws.database.RDS 59 | relates: 60 | - to: databases.follower 61 | direction: undirected 62 | - id: follower 63 | name: R/O Follower 64 | type: aws.database.RDS 65 | -------------------------------------------------------------------------------- /test/yaml/web-services-aws.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/dmytrostriletskyi/diagrams-as-code/main/json-schemas/0.0.1.json 2 | diagram: 3 | name: Web Services Architecture on AWS 4 | open: true 5 | resources: 6 | - id: dns 7 | name: DNS 8 | type: aws.network.Route53 9 | relates: 10 | - to: elb 11 | direction: outgoing 12 | - id: elb 13 | name: ELB 14 | type: aws.network.ELB 15 | relates: 16 | - to: web-services.graphql-api 17 | direction: outgoing 18 | - id: web-services 19 | name: Web Services 20 | type: cluster 21 | of: 22 | - id: graphql-api 23 | name: GraphQL API 24 | type: group 25 | of: 26 | - id: first-api 27 | name: GraphQL API №1 28 | type: aws.compute.ECS 29 | - id: second-api 30 | name: GraphQL API №2 31 | type: aws.compute.ECS 32 | - id: third-api 33 | name: GraphQL API №3 34 | type: aws.compute.ECS 35 | relates: 36 | - to: databases.leader 37 | direction: outgoing 38 | - to: memcached 39 | direction: outgoing 40 | - id: databases 41 | name: Databases 42 | type: cluster 43 | of: 44 | - id: leader 45 | name: R/W Leader 46 | type: aws.database.RDS 47 | relates: 48 | - to: databases.followers 49 | direction: undirected 50 | - id: followers 51 | name: R/O Followers 52 | type: group 53 | of: 54 | - id: first-follower 55 | name: R/O Follower №1 56 | type: aws.database.RDS 57 | - id: second-follower 58 | name: R/O Follower №2 59 | type: aws.database.RDS 60 | - id: memcached 61 | name: Memcached 62 | type: aws.database.ElastiCache 63 | -------------------------------------------------------------------------------- /test/yaml/events-processing-aws.yaml: -------------------------------------------------------------------------------- 1 | diagram: 2 | name: Events Processing Architecture on AWS 3 | open: true 4 | resources: 5 | - id: web-service 6 | name: Web Service (Source) 7 | type: aws.compute.EKS 8 | relates: 9 | - to: events-flow.workers.workers 10 | direction: outgoing 11 | - id: storage 12 | name: Events Storage 13 | type: aws.storage.S3 14 | - id: analytics 15 | name: Events Analytics 16 | type: aws.database.Redshift 17 | - id: events-flow 18 | name: Events Flow 19 | type: cluster 20 | of: 21 | - id: queue 22 | name: Events Queue 23 | type: aws.integration.SQS 24 | relates: 25 | - to: events-flow.processing.lambdas 26 | direction: outgoing 27 | - id: workers 28 | name: Workers 29 | type: cluster 30 | of: 31 | - id: workers 32 | name: Workers 33 | type: group 34 | of: 35 | - id: first-worker 36 | name: Worker №1 37 | type: aws.compute.ECS 38 | - id: second-worker 39 | name: Worker №2 40 | type: aws.compute.ECS 41 | - id: third-worker 42 | name: Worker №3 43 | type: aws.compute.ECS 44 | relates: 45 | - to: events-flow.queue 46 | direction: outgoing 47 | - id: processing 48 | name: Processing 49 | type: cluster 50 | of: 51 | - id: lambdas 52 | name: Lambdas 53 | type: group 54 | of: 55 | - id: first-process 56 | name: Lambda №1 57 | type: aws.compute.Lambda 58 | - id: second-process 59 | name: Lambda №2 60 | type: aws.compute.Lambda 61 | - id: third-process 62 | name: Lambda №3 63 | type: aws.compute.Lambda 64 | relates: 65 | - to: storage 66 | direction: outgoing 67 | - to: analytics 68 | direction: outgoing 69 | -------------------------------------------------------------------------------- /experiment/README.md: -------------------------------------------------------------------------------- 1 | # Experiment 2 | 3 | ## ilograph 4 | 5 | - Need specify `perspective` 6 | - If `depth` provided 7 | - if depth is low - need to collapse duplicated relations 8 | - One way to do it is to use deterministic serializer for all params to detect unique ones 9 | - if depth is high, some branches may not have nodes at give depth. Then use highest available node insted 10 | - if `depth` not provided, it can be calculated based on relations (max depth) 11 | - Need to select all anchestors of all nodes mentioned in relations 12 | - icons 13 | - ignore `icon` field with path 14 | - instead use `instanceOf`. Maybe use `resourceTypeSchema` (`aws.storage.S3` instead of `AWS::S3`) 15 | - add support for `import` 16 | - add validation for `Cannot contain restricted characters (/, ^, *, [, ], or ,)` etc. 17 | 18 | ## other 19 | 20 | - Show only first error (`e.errors[0].message`) or all? 21 | - Do I need `line:col`. Maybe make it configurable? 22 | - https://github.com/redhat-developer/yaml-language-server 23 | - https://adr.github.io/adr-tooling/#tooling-related-to-architecture-management 24 | - https://c4model.com/introduction 25 | 26 | ## structurizr 27 | 28 | - [scope of IDs](https://docs.structurizr.com/dsl/identifiers) 29 | - [include](https://docs.structurizr.com/dsl/includes) (aka `import`) 30 | 31 | ## general idea 32 | 33 | - enitity 34 | - nested enitities / sub-components 35 | - scope for ids / separators `::`, `.`, `/` 36 | - https://yomguithereal.github.io/mnemonist/trie-map for scope 37 | - inheritance / archetype / instanceOf 38 | - special types, such as E from ERD, user 39 | - id / label 40 | - relationships 41 | - grouping / tagging / view / context / perspective / subgraph / cluster 42 | - https://jsoncanvas.org/ 43 | - https://heptabase.com/ 44 | - styles 45 | - color, font, border, icon etc. 46 | - import 47 | - file resolution, detect cycles 48 | - render 49 | - renders specific "view" 50 | 51 | ## architecture 52 | 53 | 1. Get "AST" 54 | 2. Resolve imports, cache 55 | 3. Check ids (unique, not found) 56 | 4. Construct "Graph structure" 57 | - responsible for scope resolution 58 | - `getNode` 59 | - `getChildNodes` (gets all nested nodes) 60 | - `getParentNodes` (gets all parent nodes) 61 | - `getRelations` 62 | - etc. 63 | 5. serializer/renderer 64 | - to graphviz 65 | - to d2 66 | - to elk 67 | -------------------------------------------------------------------------------- /src/shared.ts: -------------------------------------------------------------------------------- 1 | import { 2 | LineCounter, 3 | Node, 4 | ParsedNode, 5 | parseDocument, 6 | Scalar, 7 | YAMLMap, 8 | YAMLSeq, 9 | parse as parseYaml, 10 | } from "yaml"; 11 | import { ZodError } from "zod"; 12 | import { diagramSchema, ResourceType } from "./schema.js"; 13 | import { checkAlias } from "./aliases.js"; 14 | 15 | export type Path = (string | number)[]; 16 | 17 | function traverseAst( 18 | ast: ParsedNode | null | Scalar, 19 | path: Path 20 | ): null | Node { 21 | if (path.length === 0 || ast === null) return ast; 22 | const [current, ...rest] = path; 23 | if (ast instanceof YAMLMap) { 24 | return traverseAst(ast.get(current, true) ?? null, rest); 25 | } 26 | if (ast instanceof YAMLSeq) { 27 | return traverseAst(ast.get(current, true) ?? null, rest); 28 | } 29 | throw new Error(`Unexpected node type: ${ast.constructor}`); 30 | } 31 | 32 | function getLineCol(file: string, path: Path) { 33 | const lineCounter = new LineCounter(); 34 | const ast = parseDocument(file, { keepSourceTokens: true, lineCounter }); 35 | const srcToken = traverseAst(ast.contents, path)?.srcToken; 36 | if (srcToken) return lineCounter.linePos(srcToken.offset); 37 | } 38 | 39 | export function constructError(message: string, path: Path, file?: string) { 40 | const pos = file ? getLineCol(file, path) : undefined; 41 | return new Error( 42 | `${message} ${pos ? `at ${pos.line}:${pos.col} ` : ""}(${path.join("/")})` 43 | ); 44 | } 45 | 46 | export const diagramDirection = { 47 | "left-to-right": "LR", 48 | "right-to-left": "RL", 49 | "top-to-bottom": "TB", 50 | "bottom-to-top": "BT", 51 | } as const; 52 | 53 | export const edgeDirection = { 54 | incoming: "back", 55 | outgoing: "forward", 56 | bidirectional: "both", 57 | undirected: "none", 58 | } as const; 59 | 60 | export function parse(file: string) { 61 | const rawData = parseYaml(file) as any; 62 | try { 63 | // const defaultsValues = getDefaultValues(diagramSchema); 64 | return diagramSchema.parse(rawData); 65 | } catch (e) { 66 | if (e instanceof ZodError) { 67 | const path = e.errors[0].path; 68 | if (e.errors[0].message === "Required") { 69 | const last = path.length - 1; 70 | const name = path[last]; 71 | throw constructError(`Required "${name}"`, path.slice(0, last), file); 72 | } 73 | throw constructError(e.errors[0].message, path, file); 74 | } 75 | throw e; 76 | } 77 | } 78 | 79 | export function typeToImage(type: ResourceType) { 80 | return `https://raw.githubusercontent.com/mingrammer/diagrams/refs/heads/master/resources/${checkAlias( 81 | type.split(".") 82 | ).join("/")}.png`; 83 | } 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # vitepress build output 108 | **/.vitepress/dist 109 | 110 | # vitepress cache directory 111 | **/.vitepress/cache 112 | 113 | # Docusaurus cache and generated files 114 | .docusaurus 115 | 116 | # Serverless directories 117 | .serverless/ 118 | 119 | # FuseBox cache 120 | .fusebox/ 121 | 122 | # DynamoDB Local files 123 | .dynamodb/ 124 | 125 | # TernJS port file 126 | .tern-port 127 | 128 | # Stores VSCode versions used for testing VSCode extensions 129 | .vscode-test 130 | 131 | # yarn v2 132 | .yarn/cache 133 | .yarn/unplugged 134 | .yarn/build-state.yml 135 | .yarn/install-state.gz 136 | .pnp.* 137 | -------------------------------------------------------------------------------- /test/yaml/message-collecting-gcp.yaml: -------------------------------------------------------------------------------- 1 | diagram: 2 | name: Message Collecting Architecture on GCP 3 | open: true 4 | resources: 5 | - id: pubsub 6 | name: Pubsub 7 | type: gcp.analytics.PubSub 8 | relates: 9 | - to: targets.data-flows.data-flow 10 | direction: outgoing 11 | - id: source-of-data 12 | name: Source of Data 13 | type: cluster 14 | of: 15 | - id: iot 16 | name: IoT 17 | type: group 18 | of: 19 | - id: first-iot 20 | name: Core №1 21 | type: gcp.iot.IotCore 22 | - id: second-iot 23 | name: Core №2 24 | type: gcp.iot.IotCore 25 | - id: third-iot 26 | name: Core №3 27 | type: gcp.iot.IotCore 28 | relates: 29 | - to: pubsub 30 | direction: outgoing 31 | - id: targets 32 | name: Targets 33 | type: cluster 34 | of: 35 | - id: data-flows 36 | name: Data Flow 37 | type: cluster 38 | of: 39 | - id: data-flow 40 | name: Data Flow 41 | type: gcp.analytics.Dataflow 42 | relates: 43 | - to: targets.data-lake.data-lake-group 44 | direction: outgoing 45 | - to: targets.event-driven.processing.engine 46 | direction: outgoing 47 | - to: targets.event-driven.serverless.function 48 | direction: outgoing 49 | - id: data-lake 50 | name: Data Lake 51 | type: cluster 52 | of: 53 | - id: data-lake-group 54 | name: Data Lake Group 55 | type: group 56 | of: 57 | - id: big-query 58 | name: Big Query 59 | type: gcp.analytics.BigQuery 60 | - id: storage 61 | name: Storage 62 | type: gcp.storage.GCS 63 | - id: event-driven 64 | name: Event Driven 65 | type: cluster 66 | of: 67 | - id: processing 68 | name: Processing 69 | type: cluster 70 | of: 71 | - id: engine 72 | name: Engine 73 | type: gcp.compute.AppEngine 74 | relates: 75 | - to: targets.event-driven.processing.bigtable 76 | direction: outgoing 77 | - id: bigtable 78 | name: Bigtable 79 | type: gcp.database.BigTable 80 | - id: serverless 81 | name: Serverless 82 | type: cluster 83 | of: 84 | - id: function 85 | name: Function 86 | type: gcp.compute.Functions 87 | relates: 88 | - to: targets.event-driven.serverless.application-engine 89 | direction: outgoing 90 | - id: application-engine 91 | name: Application Engine 92 | type: gcp.compute.AppEngine 93 | -------------------------------------------------------------------------------- /test/yaml/web-services-on-premise.yaml: -------------------------------------------------------------------------------- 1 | diagram: 2 | name: Web Services Architecture On-Premise 3 | open: true 4 | resources: 5 | - id: ingress 6 | name: Ingress 7 | type: onprem.network.Nginx 8 | relates: 9 | - to: web-services.graphql 10 | direction: bidirectional 11 | color: darkgreen 12 | - id: metrics 13 | name: Metrics 14 | type: onprem.monitoring.Prometheus 15 | relates: 16 | - to: monitoring 17 | direction: incoming 18 | color: firebrick 19 | style: dashed 20 | - id: monitoring 21 | name: Monitoring 22 | type: onprem.monitoring.Grafana 23 | - id: web-services 24 | name: Web Services 25 | type: cluster 26 | of: 27 | - id: graphql 28 | name: GraphQL API 29 | type: group 30 | of: 31 | - id: first-ecs 32 | name: GraphQL API №1 33 | type: onprem.compute.Server 34 | - id: second-ecs 35 | name: GraphQL API №2 36 | type: onprem.compute.Server 37 | - id: third-ecs 38 | name: GraphQL API №3 39 | type: onprem.compute.Server 40 | relates: 41 | - to: cache.leader 42 | direction: outgoing 43 | color: brown 44 | - to: databases.leader 45 | direction: outgoing 46 | color: black 47 | - to: logs-aggregator 48 | direction: outgoing 49 | color: black 50 | - id: cache 51 | name: Cache 52 | type: cluster 53 | of: 54 | - id: leader 55 | name: Leader 56 | type: onprem.inmemory.Redis 57 | relates: 58 | - to: cache.follower 59 | direction: undirected 60 | color: brown 61 | style: dashed 62 | - id: follower 63 | name: Follower 64 | type: onprem.inmemory.Redis 65 | relates: 66 | - to: metrics 67 | direction: incoming 68 | label: collect 69 | - id: databases 70 | name: Databases 71 | type: cluster 72 | of: 73 | - id: leader 74 | name: Leader 75 | type: onprem.database.PostgreSQL 76 | relates: 77 | - to: databases.follower 78 | direction: undirected 79 | color: brown 80 | style: dotted 81 | - id: follower 82 | name: Follower 83 | type: onprem.database.PostgreSQL 84 | relates: 85 | - to: metrics 86 | direction: incoming 87 | label: collect 88 | - id: logs-aggregator 89 | name: Logs Aggregator 90 | type: onprem.aggregator.Fluentd 91 | relates: 92 | - to: message-queue 93 | direction: outgoing 94 | label: parse 95 | - id: message-queue 96 | name: Message Queue 97 | type: onprem.queue.Kafka 98 | relates: 99 | - to: analytics 100 | direction: outgoing 101 | color: black 102 | style: bold 103 | - id: analytics 104 | name: Analytics 105 | type: onprem.analytics.Spark 106 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import { describe, expect, it } from "vitest"; 3 | import { Resvg } from "@resvg/resvg-js"; 4 | import { toMatchImageSnapshot } from "jest-image-snapshot"; 5 | 6 | import { parse, render } from "../src"; 7 | 8 | expect.extend({ toMatchImageSnapshot }); 9 | 10 | describe("render", () => { 11 | const files = [ 12 | "all-fields", 13 | "events-processing-aws", 14 | "exposed-pods-kubernetes", 15 | "message-collecting-gcp", 16 | "web-services-aws", 17 | "web-services-on-premise", 18 | "workers-aws", 19 | ]; 20 | files.forEach((name) => { 21 | it(`${name}.svg`, async () => { 22 | const file = await fs.readFile( 23 | new URL(`./yaml/${name}.yaml`, import.meta.url) 24 | ); 25 | 26 | await expect(render(file.toString())).toMatchFileSnapshot( 27 | `./svg/${name}.svg` 28 | ); 29 | }); 30 | 31 | // skipped because very slow 32 | it.skip(`${name}.png`, async () => { 33 | const file = await fs.readFile( 34 | new URL(`./yaml/${name}.yaml`, import.meta.url) 35 | ); 36 | 37 | const resvg = new Resvg(render(file.toString())); 38 | const resolved = await Promise.all( 39 | resvg.imagesToResolve().map(async (url) => { 40 | const img = await fetch(url); 41 | const buffer = await img.arrayBuffer(); 42 | return { 43 | url, 44 | buffer: Buffer.from(buffer), 45 | }; 46 | }) 47 | ); 48 | if (resolved.length > 0) { 49 | for (const result of resolved) { 50 | const { url, buffer } = result; 51 | resvg.resolveImage(url, buffer); 52 | } 53 | } 54 | const png = resvg.render().asPng(); 55 | 56 | // @ts-expect-error 57 | await expect(png).toMatchImageSnapshot(); 58 | }, 15_000); 59 | }); 60 | }); 61 | 62 | describe("errors", () => { 63 | it(`type error`, () => { 64 | const file = `diagram: 65 | name: Web Services Architecture on AWS 66 | resources: 67 | - id: 1`; 68 | 69 | expect(() => parse(file)).toThrowError( 70 | "Expected string, received number at 4:11 (diagram/resources/0/id)" 71 | ); 72 | }); 73 | 74 | it(`missing param`, () => { 75 | const file = `diagram: 76 | name: Web Services Architecture on AWS`; 77 | 78 | expect(() => parse(file)).toThrowError( 79 | 'Required "resources" at 2:3 (diagram)' 80 | ); 81 | }); 82 | 83 | it(`extra keys`, () => { 84 | const file = `diagram: 85 | name: Web Services Architecture on AWS 86 | resources: 87 | - id: dns 88 | name: DNS 89 | type: aws.network.Route53 90 | x: 1`; 91 | 92 | expect(() => render(file)).toThrowError( 93 | "Unrecognized key(s) in object: 'x' at 4:7 (diagram/resources/0)" 94 | ); 95 | }); 96 | 97 | it(`wrong icon`, () => { 98 | const file = `diagram: 99 | name: Web Services Architecture on AWS 100 | resources: 101 | - id: dns 102 | name: DNS 103 | type: aws.network.Route5`; 104 | 105 | expect(() => render(file)).toThrowError( 106 | 'Wrong value "aws.network.Route5". Did you mean "aws.network.Route53"? at 6:13 (diagram/resources/0/type)' 107 | ); 108 | }); 109 | 110 | it(`wrong icon 2`, () => { 111 | const file = `diagram: 112 | name: Web Services Architecture on AWS 113 | resources: 114 | - id: dns 115 | name: DNS 116 | type: aws.network`; 117 | 118 | expect(() => render(file)).toThrowError( 119 | 'Wrong value "aws.network" at 6:13 (diagram/resources/0/type)' 120 | ); 121 | }); 122 | 123 | it(`Unknown id`, () => { 124 | const file = `diagram: 125 | name: Web Services Architecture on AWS 126 | resources: 127 | - id: dns 128 | name: DNS 129 | type: aws.network.Route53 130 | relates: 131 | - to: elb 132 | direction: outgoing`; 133 | 134 | expect(() => render(file)).toThrowError( 135 | 'Unknown id "elb" at 8:15 (diagram/resources/0/relates/0/to)' 136 | ); 137 | }); 138 | 139 | it(`Duplicated ids`, () => { 140 | const file = `diagram: 141 | name: Web Services Architecture on AWS 142 | resources: 143 | - id: dns 144 | name: DNS 145 | type: aws.network.Route53 146 | - id: dns 147 | name: DNS 148 | type: aws.network.Route53`; 149 | 150 | expect(() => render(file)).toThrowError( 151 | 'Duplicated id "dns" at 7:11 (diagram/resources/1/id)' 152 | ); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /src/v1.ts: -------------------------------------------------------------------------------- 1 | import { 2 | digraph, 3 | RootGraphModel, 4 | SubgraphModel, 5 | toDot as _toDot, 6 | } from "ts-graphviz"; 7 | import { Format, Graphviz } from "@hpcc-js/wasm-graphviz"; 8 | import { Diagram, Relation, Resource } from "./schema.js"; 9 | import { 10 | constructError, 11 | diagramDirection, 12 | edgeDirection, 13 | parse, 14 | Path, 15 | typeToImage, 16 | } from "./shared.js"; 17 | 18 | type Context = { 19 | images: Set; 20 | nodes: Record; 21 | edges: Array<[from: string, rel: Relation, path: Path]>; 22 | }; 23 | 24 | function getNodes(resource: Resource): string[] { 25 | if (resource.of) return resource.of.flatMap((x) => getNodes(x)); 26 | return [resource.id]; 27 | } 28 | 29 | function iterateResources( 30 | g: SubgraphModel | RootGraphModel, 31 | resources: Resource[], 32 | context: Context, 33 | autolabel?: boolean, 34 | file?: string, 35 | prefix?: string, 36 | depth = 0, 37 | path: Path = [] 38 | ) { 39 | resources.forEach((res, i) => { 40 | const { relates, of, type, name } = res; 41 | const id = prefix ? `${prefix}.${res.id}` : res.id; 42 | const currentPath = [...path, i]; 43 | if (context.nodes[id]) { 44 | path = ["diagram", "resources", ...currentPath, "id"]; 45 | throw constructError(`Duplicated id "${id}"`, path, file); 46 | } 47 | res.id = id; 48 | context.nodes[id] = res; 49 | if (type == "cluster" || type == "group") { 50 | const __bgcolors = ["#E5F5FD", "#EBF3E7", "#ECE8F6", "#FDF7E3"]; 51 | const c = g.subgraph(type == "group" ? id : `cluster_${id}`, { 52 | style: "rounded", 53 | labeljust: "l", 54 | fontname: "Sans-Serif", 55 | fontsize: 12, 56 | label: type == "group" ? "" : name, 57 | pencolor: type == "group" ? "transparent" : "#AEB6BE", 58 | bgcolor: 59 | type == "group" ? undefined : __bgcolors[depth % __bgcolors.length], 60 | }); 61 | if (of) 62 | iterateResources( 63 | c, 64 | of, 65 | context, 66 | autolabel, 67 | file, 68 | id, 69 | depth + 1, 70 | currentPath 71 | ); 72 | } else { 73 | const label = autolabel ? type + "\n" + name : name; 74 | const padding = 0.4 * (label.split("\n").length - 1); 75 | const image = typeToImage(type); 76 | context.images.add(image); 77 | g.node(id, { 78 | label, 79 | shape: "none", 80 | height: 1.9 + padding, 81 | image, 82 | }); 83 | } 84 | relates?.forEach((rel, i) => { 85 | context.edges.push([id, rel, [...currentPath, "relates", i]]); 86 | }); 87 | }); 88 | } 89 | 90 | function process(data: Diagram, file?: string) { 91 | const context: Context = { 92 | images: new Set(), 93 | nodes: {}, 94 | edges: [], 95 | }; 96 | 97 | const graph = digraph( 98 | "G", 99 | // @ts-expect-error 100 | { 101 | label: data.diagram.name, 102 | rankdir: (data.diagram.direction 103 | ? diagramDirection[data.diagram.direction] 104 | : "LR") as any, 105 | splines: "ortho", 106 | pad: 2.0, 107 | nodesep: 0.6, 108 | ranksep: 0.75, 109 | fontname: "Sans-Serif", 110 | fontsize: 15, 111 | fontcolor: "#2D3436" as any, 112 | ...data.diagram.style?.graph, 113 | }, 114 | (g) => { 115 | g.node( 116 | // @ts-expect-error 117 | { 118 | shape: "box", 119 | style: "rounded", 120 | fixedsize: "true", 121 | width: 1.4, 122 | height: 1.4, 123 | labelloc: "b", 124 | // imagepos: "tc", 125 | imagescale: "true", 126 | fontname: "Sans-Serif", 127 | fontsize: 13, 128 | fontcolor: "#2D3436", 129 | ...data.diagram.style?.node, 130 | } 131 | ); 132 | g.edge( 133 | // @ts-expect-error 134 | { 135 | color: "#7B8894", 136 | ...data.diagram.style?.edge, 137 | } 138 | ); 139 | iterateResources( 140 | g, 141 | data.diagram.resources, 142 | context, 143 | data.diagram.label_resources, 144 | file 145 | ); 146 | 147 | context.edges.forEach(([id, rel, path]) => { 148 | const { to, direction, ...rest } = rel; 149 | const fromNode = context.nodes[id]; 150 | const toNode = context.nodes[to]; 151 | if (!toNode) { 152 | path = ["diagram", "resources", ...path, "to"]; 153 | throw constructError(`Unknown id "${to}"`, path, file); 154 | } 155 | g.edge([getNodes(fromNode), getNodes(toNode)], { 156 | dir: direction ? edgeDirection[direction] : undefined, 157 | fontcolor: "#2D3436", 158 | fontname: "Sans-Serif", 159 | fontsize: 13, 160 | ...(rest as any), 161 | }); 162 | }); 163 | } 164 | ); 165 | 166 | return { graph, images: context.images }; 167 | } 168 | 169 | export function toDot(data: Diagram, file?: string) { 170 | return _toDot(process(data, file).graph); 171 | } 172 | 173 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await#browser_compatibility 174 | // Node 14.8+ ESModules 175 | const graphviz = await Graphviz.load(); 176 | 177 | export function render(file: string, format?: Format) { 178 | const { graph, images } = process(parse(file), file); 179 | return graphviz.dot(_toDot(graph), format || "svg", { 180 | images: [...images].map((path) => ({ 181 | path, 182 | width: "256", 183 | height: "256", 184 | })), 185 | }); 186 | } 187 | -------------------------------------------------------------------------------- /src/v2.ts: -------------------------------------------------------------------------------- 1 | import { Format, Graphviz } from "@hpcc-js/wasm-graphviz"; 2 | import { Diagram, Relation, Resource } from "./schema.js"; 3 | import { 4 | constructError, 5 | diagramDirection, 6 | edgeDirection, 7 | parse, 8 | Path, 9 | typeToImage, 10 | } from "./shared.js"; 11 | import { 12 | digraph, 13 | RootGraphModel, 14 | SubgraphModel, 15 | toDot as _toDot, 16 | } from "ts-graphviz"; 17 | 18 | export class DiagramResolver { 19 | #raw: Diagram; 20 | #file?: string; 21 | #nodes: Record = {}; 22 | #edges: Array<[from: string, rel: Relation, path: Path]> = []; 23 | 24 | constructor(d: Diagram, file?: string) { 25 | this.#raw = d; 26 | this.#file = file; 27 | } 28 | 29 | getResource(id: string): Resource { 30 | return this.#nodes[id]; 31 | } 32 | 33 | getLeafResourceIds(id: string): string[] { 34 | function getNodes(resource: Resource): string[] { 35 | if (resource.of) return resource.of.flatMap((x) => getNodes(x)); 36 | return [resource.id]; 37 | } 38 | return getNodes(this.getResource(id)); 39 | } 40 | 41 | reduceResources( 42 | cb: (res: Resource, prev: T, depth: number) => T | undefined, 43 | root: T 44 | ) { 45 | const recursive = ( 46 | g: T, 47 | resources: Resource[], 48 | prefix?: string, 49 | depth = 0, 50 | path: Path = [] 51 | ) => { 52 | resources.forEach((res, i) => { 53 | const { relates, of } = res; 54 | const id = prefix ? `${prefix}.${res.id}` : res.id; 55 | let currentPath = [...path, i]; 56 | if (this.#nodes[id]) { 57 | currentPath = ["diagram", "resources", ...currentPath, "id"]; 58 | throw constructError( 59 | `Duplicated id "${id}"`, 60 | currentPath, 61 | this.#file 62 | ); 63 | } 64 | res.id = id; 65 | this.#nodes[id] = res; 66 | const prev = cb(res, g, depth); 67 | if (of && prev) { 68 | recursive(prev, of, id, depth + 1, currentPath); 69 | } 70 | relates?.forEach((rel, i) => { 71 | this.#edges.push([id, rel, [...currentPath, "relates", i]]); 72 | }); 73 | }); 74 | }; 75 | 76 | recursive(root, this.#raw.diagram.resources); 77 | } 78 | 79 | iterateRelations(cb: (from: string[], to: string[], rel: Relation) => void) { 80 | this.#edges.forEach(([id, rel, path]) => { 81 | const { to } = rel; 82 | const toNode = this.getResource(to); 83 | if (!toNode) { 84 | path = ["diagram", "resources", ...path, "to"]; 85 | throw constructError(`Unknown id "${to}"`, path, this.#file); 86 | } 87 | cb(this.getLeafResourceIds(id), this.getLeafResourceIds(to), rel); 88 | }); 89 | } 90 | } 91 | 92 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await#browser_compatibility 93 | // Node 14.8+ ESModules 94 | const graphviz = await Graphviz.load(); 95 | 96 | export class RenderGraphviz { 97 | #resolver: DiagramResolver; 98 | #root: RootGraphModel; 99 | #images: Set = new Set(); 100 | 101 | constructor(file: string) { 102 | const data = parse(file); 103 | const autolabel = data.diagram.label_resources; 104 | this.#resolver = new DiagramResolver(data, file); 105 | 106 | this.#root = digraph( 107 | "G", 108 | // @ts-expect-error 109 | { 110 | label: data.diagram.name, 111 | rankdir: (data.diagram.direction 112 | ? diagramDirection[data.diagram.direction] 113 | : "LR") as any, 114 | splines: "ortho", 115 | pad: 2.0, 116 | nodesep: 0.6, 117 | ranksep: 0.75, 118 | fontname: "Sans-Serif", 119 | fontsize: 15, 120 | fontcolor: "#2D3436" as any, 121 | ...data.diagram.style?.graph, 122 | }, 123 | () => {} 124 | ); 125 | this.#root.node( 126 | // @ts-expect-error 127 | { 128 | shape: "box", 129 | style: "rounded", 130 | fixedsize: "true", 131 | width: 1.4, 132 | height: 1.4, 133 | labelloc: "b", 134 | // imagepos: "tc", 135 | imagescale: "true", 136 | fontname: "Sans-Serif", 137 | fontsize: 13, 138 | fontcolor: "#2D3436", 139 | ...data.diagram.style?.node, 140 | } 141 | ); 142 | this.#root.edge( 143 | // @ts-expect-error 144 | { 145 | color: "#7B8894", 146 | ...data.diagram.style?.edge, 147 | } 148 | ); 149 | this.#resolver.reduceResources((res, g, depth) => { 150 | const { type, name, id } = res; 151 | if (type == "cluster" || type == "group") { 152 | const __bgcolors = ["#E5F5FD", "#EBF3E7", "#ECE8F6", "#FDF7E3"]; 153 | return g.subgraph(type == "group" ? id : `cluster_${id}`, { 154 | style: "rounded", 155 | labeljust: "l", 156 | fontname: "Sans-Serif", 157 | fontsize: 12, 158 | label: type == "group" ? "" : name, 159 | pencolor: type == "group" ? "transparent" : "#AEB6BE", 160 | bgcolor: 161 | type == "group" ? undefined : __bgcolors[depth % __bgcolors.length], 162 | }); 163 | } else { 164 | const label = autolabel ? type + "\n" + name : name; 165 | const padding = 0.4 * (label.split("\n").length - 1); 166 | const image = typeToImage(type); 167 | this.#images.add(image); 168 | 169 | g.node(id, { 170 | label, 171 | shape: "none", 172 | height: 1.9 + padding, 173 | image, 174 | }); 175 | } 176 | }, this.#root as SubgraphModel | RootGraphModel); 177 | 178 | this.#resolver.iterateRelations((from, to, rel) => { 179 | const { to: _, direction, ...rest } = rel; 180 | this.#root.edge([from, to], { 181 | dir: direction ? edgeDirection[direction] : undefined, 182 | fontcolor: "#2D3436", 183 | fontname: "Sans-Serif", 184 | fontsize: 13, 185 | ...(rest as any), 186 | }); 187 | }); 188 | } 189 | 190 | toDot(): string { 191 | return _toDot(this.#root); 192 | } 193 | 194 | render(format?: Format) { 195 | return graphviz.dot(this.toDot(), format || "svg", { 196 | images: [...this.#images].map((path) => ({ 197 | path, 198 | width: "256", 199 | height: "256", 200 | })), 201 | }); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /test/svg/all-fields.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | G 11 | 12 | Web Services Architecture on AWS 13 | 14 | cluster_web-services 15 | 16 | Web Services 17 | 18 | 19 | cluster_databases 20 | 21 | Databases 22 | 23 | 24 | 25 | dns 26 | 27 | DNS 28 | 29 | 30 | 31 | elb 32 | 33 | ELB 34 | 35 | 36 | 37 | dns->elb 38 | 39 | 40 | Makes Request 41 | 42 | 43 | 44 | web-services.graphql-api.first-ecs 45 | 46 | GraphQL API №1 47 | 48 | 49 | 50 | elb->web-services.graphql-api.first-ecs 51 | 52 | 53 | Proxy Request 54 | 55 | 56 | 57 | web-services.graphql-api.second-ecs 58 | 59 | GraphQL API №2 60 | 61 | 62 | 63 | elb->web-services.graphql-api.second-ecs 64 | 65 | 66 | Proxy Request 67 | 68 | 69 | 70 | databases.leader 71 | 72 | R/W Leader 73 | 74 | 75 | 76 | web-services.graphql-api.first-ecs->databases.leader 77 | 78 | 79 | 80 | 81 | 82 | web-services.graphql-api.second-ecs->databases.leader 83 | 84 | 85 | 86 | 87 | 88 | databases.follower 89 | 90 | R/O Follower 91 | 92 | 93 | 94 | databases.leader->databases.follower 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /test/svg/exposed-pods-kubernetes.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | G 11 | 12 | Exposed Pods Architecture on Kubernetes 13 | 14 | 15 | ingress 16 | 17 | Ingress 18 | 19 | 20 | 21 | service 22 | 23 | Service 24 | 25 | 26 | 27 | ingress->service 28 | 29 | 30 | 31 | 32 | 33 | pods.first-pod 34 | 35 | Pod №1 36 | 37 | 38 | 39 | service->pods.first-pod 40 | 41 | 42 | 43 | 44 | 45 | pods.second-pod 46 | 47 | Pod №2 48 | 49 | 50 | 51 | service->pods.second-pod 52 | 53 | 54 | 55 | 56 | 57 | pods.third-pod 58 | 59 | Pod №3 60 | 61 | 62 | 63 | service->pods.third-pod 64 | 65 | 66 | 67 | 68 | 69 | replica-set 70 | 71 | Replica Set 72 | 73 | 74 | 75 | deployment 76 | 77 | Deployment 78 | 79 | 80 | 81 | replica-set->deployment 82 | 83 | 84 | 85 | 86 | 87 | hpa 88 | 89 | HPA 90 | 91 | 92 | 93 | deployment->hpa 94 | 95 | 96 | 97 | 98 | 99 | pods.first-pod->replica-set 100 | 101 | 102 | 103 | 104 | 105 | pods.second-pod->replica-set 106 | 107 | 108 | 109 | 110 | 111 | pods.third-pod->replica-set 112 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /src/aliases.ts: -------------------------------------------------------------------------------- 1 | const aliases: Record>> = { 2 | onprem: { 3 | ci: { 4 | tc: "teamcity", 5 | }, 6 | network: { 7 | osm: "open-service-mesh", 8 | }, 9 | proxmox: { 10 | proxmoxve: "pve", 11 | }, 12 | storage: { 13 | ceph_osd: "ceph-osd", 14 | }, 15 | }, 16 | aws: { 17 | analytics: { 18 | es: "elasticsearch-service", 19 | }, 20 | business: { 21 | a4b: "alexa-for-business", 22 | }, 23 | blockchain: { 24 | qldb: "quantum-ledger-database-qldb", 25 | }, 26 | compute: { 27 | autoscaling: "application-auto-scaling", 28 | ami: "ec2-ami", 29 | ecr: "ec2-container-registry", 30 | eb: "elastic-beanstalk", 31 | ecs: "elastic-container-service", 32 | eks: "elastic-kubernetes-service", 33 | sar: "serverless-application-repository", 34 | }, 35 | database: { 36 | dms: "database-migration-service", 37 | documentdb: "documentdb-mongodb-compatibility", 38 | dax: "dynamodb-dax", 39 | dynamodbgsi: "dynamodb-global-secondary-index", 40 | db: "database", 41 | ddb: "dynamodb", 42 | elasticache: "elasticache", 43 | qldb: "quantum-ledger-database-qldb", 44 | }, 45 | devtools: { 46 | cli: "command-line-interface", 47 | devtools: "developer-tools", 48 | }, 49 | engagement: { 50 | ses: "simple-email-service-ses", 51 | }, 52 | general: { 53 | officebuilding: "generic-office-building", 54 | }, 55 | integration: { 56 | sns: "simple-notification-service-sns", 57 | sqs: "simple-queue-service-sqs", 58 | sf: "step-functions", 59 | }, 60 | iot: { 61 | freertos: "freertos", 62 | iotboard: "iot-hardware-board", 63 | }, 64 | management: { 65 | ssm: "systems-manager", 66 | parameterstore: "systems-manager-parameter-store", 67 | }, 68 | migration: { 69 | ads: "application-discovery-service", 70 | cem: "cloudendure-migration", 71 | dms: "database-migration-service", 72 | mat: "migration-and-transfer", 73 | sms: "server-migration-service", 74 | }, 75 | ml: { 76 | dlc: "deep-learning-containers", 77 | }, 78 | network: { 79 | cf: "cloud-front", 80 | elb: "elastic-load-balancing", 81 | alb: "elb-application-load-balancer", 82 | clb: "elb-classic-load-balancer", 83 | nlb: "elb-network-load-balancer", 84 | gax: "global-accelerator", 85 | igw: "internet-gateway", 86 | tgw: "transit-gateway", 87 | tgwattach: "transit-gateway-attachment", 88 | route53: "route-53", 89 | }, 90 | security: { 91 | acm: "certificate-manager", 92 | cloudhsm: "cloudhsm", 93 | ds: "directory-service", 94 | fms: "firewall-manager", 95 | iamaccessanalyzer: "identity-and-access-management-iam-access-analyzer", 96 | iamawssts: "identity-and-access-management-iam-aws-sts", 97 | iampermissions: "identity-and-access-management-iam-permissions", 98 | iamrole: "identity-and-access-management-iam-role", 99 | iam: "identity-and-access-management-iam", 100 | kms: "key-management-service", 101 | ram: "resource-access-manager", 102 | }, 103 | storage: { 104 | cdr: "cloudendure-disaster-recovery", 105 | ebs: "elastic-block-store-ebs", 106 | efs: "elastic-file-system-efs", 107 | fsx: "fsx", 108 | s3: "simple-storage-service-s3", 109 | }, 110 | }, 111 | azure: { 112 | compute: { 113 | acr: "container-registries", 114 | aks: "kubernetes-services", 115 | vmss: "vm-scale-set", 116 | }, 117 | }, 118 | gcp: { 119 | compute: { 120 | gae: "app-engine", 121 | appengine: "app-engine", 122 | gcf: "functions", 123 | gce: "compute-engine", 124 | gke: "kubernetes-engine", 125 | }, 126 | devtools: { 127 | gcr: "container-registry", 128 | }, 129 | ml: { 130 | nlapi: "natural-language-api", 131 | stt: "speech-to-text", 132 | tts: "text-to-speech", 133 | }, 134 | network: { 135 | vpc: "virtual-private-cloud", 136 | }, 137 | security: { 138 | kms: "key-management-service", 139 | scc: "security-command-center", 140 | }, 141 | storage: { 142 | gcs: "storage", 143 | }, 144 | iot: { 145 | iotcore: "iot-core" 146 | } 147 | }, 148 | firebase: { 149 | grow: { 150 | fcm: "messaging", 151 | }, 152 | }, 153 | k8s: { 154 | clusterconfig: { 155 | limitrange: "limits", 156 | horizontalpodautoscaler: "hpa", 157 | }, 158 | compute: { 159 | deployment: "deploy", 160 | daemonset: "ds", 161 | replicaset: "rs", 162 | statefulset: "sts", 163 | }, 164 | controlplane: { 165 | apiserver: "api", 166 | controllermanager: "cm", 167 | kubeproxy: "k-proxy", 168 | scheduler: "sched", 169 | }, 170 | group: { 171 | namespace: "ns", 172 | }, 173 | network: { 174 | endpoint: "ep", 175 | ingress: "ing", 176 | networkpolicy: "netpol", 177 | service: "svc", 178 | }, 179 | podconfig: { 180 | configmap: "cm", 181 | }, 182 | rbac: { 183 | clusterrole: "c-role", 184 | clusterrolebinding: "crb", 185 | rolebinding: "rb", 186 | serviceaccount: "sa", 187 | }, 188 | storage: { 189 | persistentvolume: "pv", 190 | persistentvolumeclaim: "pvc", 191 | storageclass: "sc", 192 | volume: "vol", 193 | }, 194 | }, 195 | alibabacloud: { 196 | application: { 197 | sls: "log-service", 198 | mns: "message-notification-service", 199 | pts: "performance-testing-service", 200 | sca: "smart-conversation-analysis", 201 | }, 202 | compute: { 203 | ess: "auto-scaling", 204 | ecs: "elastic-compute-service", 205 | eci: "elastic-container-instance", 206 | ehpc: "elastic-high-performance-computing", 207 | fc: "function-compute", 208 | oos: "operation-orchestration-service", 209 | ros: "resource-orchestration-service", 210 | slb: "server-load-balancer", 211 | sae: "serverless-app-engine", 212 | sas: "simple-application-server", 213 | was: "web-app-service", 214 | }, 215 | database: { 216 | dms: "data-management-service", 217 | dts: "data-transmission-service", 218 | dbs: "database-backup-service", 219 | drds: "disribute-relational-database-service", 220 | gds: "graph-database-service", 221 | rds: "relational-database-service", 222 | }, 223 | network: { 224 | cen: "cloud-enterprise-network", 225 | eip: "elastic-ip-address", 226 | slb: "server-load-balancer", 227 | vpc: "virtual-private-cloud", 228 | }, 229 | security: { 230 | abs: "anti-bot-service", 231 | as: "antifraud-service", 232 | cfw: "cloud-firewall", 233 | cm: "content-moderation", 234 | des: "data-encryption-service", 235 | waf: "web-application-firewall", 236 | }, 237 | storage: { 238 | hdfs: "file-storage-hdfs", 239 | nas: "file-storage-nas", 240 | hbr: "hybrid-backup-recovery", 241 | hdr: "hybrid-cloud-disaster-recovery", 242 | oss: "object-storage-service", 243 | ots: "object-table-store", 244 | }, 245 | }, 246 | oci: { 247 | compute: { 248 | virtualmachine: "vm", 249 | virtualmachinewhite: "vm-white", 250 | baremetal: "bm", 251 | baremetalwhite: "bm-white", 252 | ociregistry: "ocir", 253 | ociregistrywhite: "ocir-white", 254 | containerengine: "oke", 255 | containerenginewhite: "oke-white", 256 | }, 257 | database: { 258 | adb: "autonomous", 259 | adbwhite: "autonomous-white", 260 | dbservice: "database-service", 261 | dbservicewhite: "database-service-white", 262 | }, 263 | }, 264 | elastic: { 265 | elasticsearch: { 266 | ml: "machine-learning", 267 | }, 268 | }, 269 | openstack: { 270 | deployment: { 271 | kollaansible: "kolla", 272 | }, 273 | }, 274 | }; 275 | 276 | export function checkAlias(parts: string[]) { 277 | parts = parts.map((x) => x.toLowerCase()); 278 | if (parts.length === 3) { 279 | const last = aliases[parts[0]]?.[parts[1]]?.[parts[2]]; 280 | if (last) parts[2] = last; 281 | } 282 | return parts; 283 | } 284 | -------------------------------------------------------------------------------- /test/svg/workers-aws.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | G 11 | 12 | Workers Architecture on AWS 13 | 14 | 15 | elb 16 | 17 | ELB 18 | 19 | 20 | 21 | workers.first-worker 22 | 23 | Worker №1 24 | 25 | 26 | 27 | elb->workers.first-worker 28 | 29 | 30 | 31 | 32 | 33 | workers.second-worker 34 | 35 | Worker №2 36 | 37 | 38 | 39 | elb->workers.second-worker 40 | 41 | 42 | 43 | 44 | 45 | workers.third-worker 46 | 47 | Worker №3 48 | 49 | 50 | 51 | elb->workers.third-worker 52 | 53 | 54 | 55 | 56 | 57 | workers.fourth-worker 58 | 59 | Worker №4 60 | 61 | 62 | 63 | elb->workers.fourth-worker 64 | 65 | 66 | 67 | 68 | 69 | workers.fifth-worker 70 | 71 | Worker №5 72 | 73 | 74 | 75 | elb->workers.fifth-worker 76 | 77 | 78 | 79 | 80 | 81 | workers.sixth-worker 82 | 83 | Worker №6 84 | 85 | 86 | 87 | elb->workers.sixth-worker 88 | 89 | 90 | 91 | 92 | 93 | database 94 | 95 | Events 96 | 97 | 98 | 99 | workers.first-worker->database 100 | 101 | 102 | 103 | 104 | 105 | workers.second-worker->database 106 | 107 | 108 | 109 | 110 | 111 | workers.third-worker->database 112 | 113 | 114 | 115 | 116 | 117 | workers.fourth-worker->database 118 | 119 | 120 | 121 | 122 | 123 | workers.fifth-worker->database 124 | 125 | 126 | 127 | 128 | 129 | workers.sixth-worker->database 130 | 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /test/svg/web-services-aws.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | G 11 | 12 | Web Services Architecture on AWS 13 | 14 | cluster_web-services 15 | 16 | Web Services 17 | 18 | 19 | cluster_databases 20 | 21 | Databases 22 | 23 | 24 | 25 | dns 26 | 27 | DNS 28 | 29 | 30 | 31 | elb 32 | 33 | ELB 34 | 35 | 36 | 37 | dns->elb 38 | 39 | 40 | 41 | 42 | 43 | web-services.graphql-api.first-api 44 | 45 | GraphQL API №1 46 | 47 | 48 | 49 | elb->web-services.graphql-api.first-api 50 | 51 | 52 | 53 | 54 | 55 | web-services.graphql-api.second-api 56 | 57 | GraphQL API №2 58 | 59 | 60 | 61 | elb->web-services.graphql-api.second-api 62 | 63 | 64 | 65 | 66 | 67 | web-services.graphql-api.third-api 68 | 69 | GraphQL API №3 70 | 71 | 72 | 73 | elb->web-services.graphql-api.third-api 74 | 75 | 76 | 77 | 78 | 79 | memcached 80 | 81 | Memcached 82 | 83 | 84 | 85 | web-services.graphql-api.first-api->memcached 86 | 87 | 88 | 89 | 90 | 91 | databases.leader 92 | 93 | R/W Leader 94 | 95 | 96 | 97 | web-services.graphql-api.first-api->databases.leader 98 | 99 | 100 | 101 | 102 | 103 | web-services.graphql-api.second-api->memcached 104 | 105 | 106 | 107 | 108 | 109 | web-services.graphql-api.second-api->databases.leader 110 | 111 | 112 | 113 | 114 | 115 | web-services.graphql-api.third-api->memcached 116 | 117 | 118 | 119 | 120 | 121 | web-services.graphql-api.third-api->databases.leader 122 | 123 | 124 | 125 | 126 | 127 | databases.followers.first-follower 128 | 129 | R/O Follower №1 130 | 131 | 132 | 133 | databases.leader->databases.followers.first-follower 134 | 135 | 136 | 137 | 138 | databases.followers.second-follower 139 | 140 | R/O Follower №2 141 | 142 | 143 | 144 | databases.leader->databases.followers.second-follower 145 | 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /test/svg/events-processing-aws.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | G 11 | 12 | Events Processing Architecture on AWS 13 | 14 | cluster_events-flow 15 | 16 | Events Flow 17 | 18 | 19 | cluster_events-flow.workers 20 | 21 | Workers 22 | 23 | 24 | cluster_events-flow.processing 25 | 26 | Processing 27 | 28 | 29 | 30 | web-service 31 | 32 | Web Service (Source) 33 | 34 | 35 | 36 | events-flow.workers.workers.first-worker 37 | 38 | Worker №1 39 | 40 | 41 | 42 | web-service->events-flow.workers.workers.first-worker 43 | 44 | 45 | 46 | 47 | 48 | events-flow.workers.workers.second-worker 49 | 50 | Worker №2 51 | 52 | 53 | 54 | web-service->events-flow.workers.workers.second-worker 55 | 56 | 57 | 58 | 59 | 60 | events-flow.workers.workers.third-worker 61 | 62 | Worker №3 63 | 64 | 65 | 66 | web-service->events-flow.workers.workers.third-worker 67 | 68 | 69 | 70 | 71 | 72 | storage 73 | 74 | Events Storage 75 | 76 | 77 | 78 | analytics 79 | 80 | Events Analytics 81 | 82 | 83 | 84 | events-flow.queue 85 | 86 | Events Queue 87 | 88 | 89 | 90 | events-flow.processing.lambdas.first-process 91 | 92 | Lambda №1 93 | 94 | 95 | 96 | events-flow.queue->events-flow.processing.lambdas.first-process 97 | 98 | 99 | 100 | 101 | 102 | events-flow.processing.lambdas.second-process 103 | 104 | Lambda №2 105 | 106 | 107 | 108 | events-flow.queue->events-flow.processing.lambdas.second-process 109 | 110 | 111 | 112 | 113 | 114 | events-flow.processing.lambdas.third-process 115 | 116 | Lambda №3 117 | 118 | 119 | 120 | events-flow.queue->events-flow.processing.lambdas.third-process 121 | 122 | 123 | 124 | 125 | 126 | events-flow.workers.workers.first-worker->events-flow.queue 127 | 128 | 129 | 130 | 131 | 132 | events-flow.workers.workers.second-worker->events-flow.queue 133 | 134 | 135 | 136 | 137 | 138 | events-flow.workers.workers.third-worker->events-flow.queue 139 | 140 | 141 | 142 | 143 | 144 | events-flow.processing.lambdas.first-process->storage 145 | 146 | 147 | 148 | 149 | 150 | events-flow.processing.lambdas.first-process->analytics 151 | 152 | 153 | 154 | 155 | 156 | events-flow.processing.lambdas.second-process->storage 157 | 158 | 159 | 160 | 161 | 162 | events-flow.processing.lambdas.second-process->analytics 163 | 164 | 165 | 166 | 167 | 168 | events-flow.processing.lambdas.third-process->storage 169 | 170 | 171 | 172 | 173 | 174 | events-flow.processing.lambdas.third-process->analytics 175 | 176 | 177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /test/svg/message-collecting-gcp.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | G 11 | 12 | Message Collecting Architecture on GCP 13 | 14 | cluster_source-of-data 15 | 16 | Source of Data 17 | 18 | 19 | cluster_targets 20 | 21 | Targets 22 | 23 | 24 | cluster_targets.data-flows 25 | 26 | Data Flow 27 | 28 | 29 | cluster_targets.data-lake 30 | 31 | Data Lake 32 | 33 | 34 | cluster_targets.event-driven 35 | 36 | Event Driven 37 | 38 | 39 | cluster_targets.event-driven.processing 40 | 41 | Processing 42 | 43 | 44 | cluster_targets.event-driven.serverless 45 | 46 | Serverless 47 | 48 | 49 | 50 | pubsub 51 | 52 | Pubsub 53 | 54 | 55 | 56 | targets.data-flows.data-flow 57 | 58 | Data Flow 59 | 60 | 61 | 62 | pubsub->targets.data-flows.data-flow 63 | 64 | 65 | 66 | 67 | 68 | source-of-data.iot.first-iot 69 | 70 | Core №1 71 | 72 | 73 | 74 | source-of-data.iot.first-iot->pubsub 75 | 76 | 77 | 78 | 79 | 80 | source-of-data.iot.second-iot 81 | 82 | Core №2 83 | 84 | 85 | 86 | source-of-data.iot.second-iot->pubsub 87 | 88 | 89 | 90 | 91 | 92 | source-of-data.iot.third-iot 93 | 94 | Core №3 95 | 96 | 97 | 98 | source-of-data.iot.third-iot->pubsub 99 | 100 | 101 | 102 | 103 | 104 | targets.data-lake.data-lake-group.big-query 105 | 106 | Big Query 107 | 108 | 109 | 110 | targets.data-flows.data-flow->targets.data-lake.data-lake-group.big-query 111 | 112 | 113 | 114 | 115 | 116 | targets.data-lake.data-lake-group.storage 117 | 118 | Storage 119 | 120 | 121 | 122 | targets.data-flows.data-flow->targets.data-lake.data-lake-group.storage 123 | 124 | 125 | 126 | 127 | 128 | targets.event-driven.processing.engine 129 | 130 | Engine 131 | 132 | 133 | 134 | targets.data-flows.data-flow->targets.event-driven.processing.engine 135 | 136 | 137 | 138 | 139 | 140 | targets.event-driven.serverless.function 141 | 142 | Function 143 | 144 | 145 | 146 | targets.data-flows.data-flow->targets.event-driven.serverless.function 147 | 148 | 149 | 150 | 151 | 152 | targets.event-driven.processing.bigtable 153 | 154 | Bigtable 155 | 156 | 157 | 158 | targets.event-driven.processing.engine->targets.event-driven.processing.bigtable 159 | 160 | 161 | 162 | 163 | 164 | targets.event-driven.serverless.application-engine 165 | 166 | Application Engine 167 | 168 | 169 | 170 | targets.event-driven.serverless.function->targets.event-driven.serverless.application-engine 171 | 172 | 173 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /test/svg/web-services-on-premise.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | G 11 | 12 | Web Services Architecture On-Premise 13 | 14 | cluster_web-services 15 | 16 | Web Services 17 | 18 | 19 | cluster_cache 20 | 21 | Cache 22 | 23 | 24 | cluster_databases 25 | 26 | Databases 27 | 28 | 29 | 30 | ingress 31 | 32 | Ingress 33 | 34 | 35 | 36 | web-services.graphql.first-ecs 37 | 38 | GraphQL API №1 39 | 40 | 41 | 42 | ingress->web-services.graphql.first-ecs 43 | 44 | 45 | 46 | 47 | 48 | 49 | web-services.graphql.second-ecs 50 | 51 | GraphQL API №2 52 | 53 | 54 | 55 | ingress->web-services.graphql.second-ecs 56 | 57 | 58 | 59 | 60 | 61 | 62 | web-services.graphql.third-ecs 63 | 64 | GraphQL API №3 65 | 66 | 67 | 68 | ingress->web-services.graphql.third-ecs 69 | 70 | 71 | 72 | 73 | 74 | 75 | metrics 76 | 77 | Metrics 78 | 79 | 80 | 81 | monitoring 82 | 83 | Monitoring 84 | 85 | 86 | 87 | metrics->monitoring 88 | 89 | 90 | 91 | 92 | 93 | logs-aggregator 94 | 95 | Logs Aggregator 96 | 97 | 98 | 99 | message-queue 100 | 101 | Message Queue 102 | 103 | 104 | 105 | logs-aggregator->message-queue 106 | 107 | 108 | parse 109 | 110 | 111 | 112 | analytics 113 | 114 | Analytics 115 | 116 | 117 | 118 | message-queue->analytics 119 | 120 | 121 | 122 | 123 | 124 | web-services.graphql.first-ecs->logs-aggregator 125 | 126 | 127 | 128 | 129 | 130 | cache.leader 131 | 132 | Leader 133 | 134 | 135 | 136 | web-services.graphql.first-ecs->cache.leader 137 | 138 | 139 | 140 | 141 | 142 | databases.leader 143 | 144 | Leader 145 | 146 | 147 | 148 | web-services.graphql.first-ecs->databases.leader 149 | 150 | 151 | 152 | 153 | 154 | web-services.graphql.second-ecs->logs-aggregator 155 | 156 | 157 | 158 | 159 | 160 | web-services.graphql.second-ecs->cache.leader 161 | 162 | 163 | 164 | 165 | 166 | web-services.graphql.second-ecs->databases.leader 167 | 168 | 169 | 170 | 171 | 172 | web-services.graphql.third-ecs->logs-aggregator 173 | 174 | 175 | 176 | 177 | 178 | web-services.graphql.third-ecs->cache.leader 179 | 180 | 181 | 182 | 183 | 184 | web-services.graphql.third-ecs->databases.leader 185 | 186 | 187 | 188 | 189 | 190 | cache.follower 191 | 192 | Follower 193 | 194 | 195 | 196 | cache.leader->cache.follower 197 | 198 | 199 | 200 | 201 | cache.follower->metrics 202 | 203 | 204 | collect 205 | 206 | 207 | 208 | databases.follower 209 | 210 | Follower 211 | 212 | 213 | 214 | databases.leader->databases.follower 215 | 216 | 217 | 218 | 219 | databases.follower->metrics 220 | 221 | 222 | collect 223 | 224 | 225 | 226 | -------------------------------------------------------------------------------- /experiment/ilograph.ts: -------------------------------------------------------------------------------- 1 | // https://www.ilograph.com/docs/spec/ 2 | 3 | import { z } from "zod"; 4 | 5 | const arrowDirectionSchema = z 6 | .enum(["forward", "backward", "bidirectional"]) 7 | .describe( 8 | "The arrow direction for this relation. The direction specified is relative to the perspective’s orientation. Accepted values are forward, backward, and bidirectional. Defaults to forward." 9 | ) 10 | .optional() 11 | .default("forward"); 12 | 13 | const importSchema = z 14 | .object({ 15 | from: z.string().describe("The diagram to import"), 16 | namespace: z 17 | .string() 18 | .describe( 19 | "Namespace prefix for imported resources and perspectives. Cannot contain restricted characters (/, ^, *, [, ], or ,)" 20 | ), 21 | }) 22 | .strict(); 23 | 24 | const layoutSchema = z 25 | .object({ 26 | compactness: z 27 | .number() 28 | .describe( 29 | "A number from 0.1 to 1 indicating how compactly child resources are rendered. Affects only context resources with more than one child. Defaults to 1" 30 | ) 31 | .optional(), 32 | // .default(1), 33 | sizes: z 34 | .enum(["proportional", "uniform", "auto"]) 35 | .describe( 36 | "If set to proportional, child resource sizes are always proportional to the number of resources they are related to. If set to uniform, all children always have the same size. If set to auto, the layout engine automatically chooses on a per-perspective basis. Defaults to auto" 37 | ) 38 | .optional(), 39 | // .default("auto"), 40 | }) 41 | .strict(); 42 | 43 | const contextEntryBaseSchema = z 44 | .object({ 45 | resourceId: z 46 | .string() 47 | .describe( 48 | "The identifier (or comma-separated identifiers) for this entry" 49 | ), 50 | }) 51 | .strict(); 52 | 53 | type ContextEntry = z.infer & { 54 | children?: ContextEntry[]; 55 | }; 56 | 57 | const contextEntrySchema: z.ZodType = contextEntryBaseSchema 58 | .extend({ 59 | children: z 60 | .lazy(() => contextEntrySchema.array()) 61 | .describe( 62 | "An array of child context entries. Cannot be defined if more than one resource is specified in resourceId" 63 | ) 64 | .optional(), 65 | }) 66 | .strict(); 67 | 68 | const contextSchema = z 69 | .object({ 70 | name: z 71 | .string() 72 | .describe( 73 | "The name of the context. Must be unique and cannot be “Default”" 74 | ), 75 | roots: z.array(contextEntrySchema).optional(), 76 | extends: z 77 | .string() 78 | .describe( 79 | "The previously-defined context (or comma-separated contexts) that this context extends" 80 | ) 81 | .optional(), 82 | hidden: z 83 | .boolean() 84 | .describe("If true, the context is not shown in the context dropdown") 85 | .optional() 86 | .default(false), 87 | }) 88 | .strict(); 89 | 90 | const slideSchema = z 91 | .object({ 92 | text: z 93 | .string() 94 | .describe( 95 | "The text accompanying the slide. If not present, the previous slide’s text is used. Has support for markdown, and can contain multiple lines." 96 | ) 97 | .optional(), 98 | select: z 99 | .string() 100 | .describe( 101 | "The resource identifier (or comma-separated identifiers) to select. Selected resource(s) are displayed more prominently, and unrelated resource are hidden. If not present, the previous slide’s select value is used. Use ^ to not select any resource" 102 | ) 103 | .optional(), 104 | expand: z 105 | .string() 106 | .describe( 107 | "The resource identifier to expand (make full-screen). If not present, the previous slide’s expand value is used. Use ^ to not expand any resource." 108 | ) 109 | .optional(), 110 | highlight: z 111 | .string() 112 | .describe( 113 | "The resource identifier (or comma-separated identifiers) to highlight. Highlighted resource(s) have an animated border, and unrelated resource are faded out" 114 | ) 115 | .optional(), 116 | detail: z 117 | .number() 118 | .describe( 119 | "The level of detail used during the slide. Can be between 0.001 (very low detail) and 1 (full detail). Defaults to 1 if not present" 120 | ) 121 | .optional() 122 | .default(1), 123 | }) 124 | .strict(); 125 | 126 | const overrideSchema = z 127 | .object({ 128 | resourceId: z 129 | .string() 130 | .describe( 131 | "The resource identifier (or comma-separated identifiers) to override" 132 | ), 133 | parentId: z 134 | .string() 135 | .describe( 136 | "Specify to assign a different parent to this resource for the perspective, or none to assign no parent" 137 | ) 138 | .optional(), 139 | scale: z 140 | .number() 141 | .describe( 142 | "Specify to scale the size of this resource for the perspective. https://www.ilograph.com/docs/editing/perspectives/resource-sizes-and-positions/#adjusting-resource-sizes" 143 | ) 144 | .optional(), 145 | }) 146 | .strict() 147 | .describe( 148 | "Perspective overrides are used to override parent of resources in a perspective. This is handy for showing resources in different contexts." 149 | ); 150 | 151 | const aliasSchema = z 152 | .object({ 153 | alias: z 154 | .string() 155 | .describe( 156 | "The identifier for this alias. Can be used to override an existing resource id. Cannot contain /, ^, *, [, ], or , characters" 157 | ), 158 | for: z 159 | .string() 160 | .describe( 161 | "The value of this alias. Typically is a comma-seperated list of identifiers" 162 | ), 163 | }) 164 | .strict() 165 | .describe( 166 | "https://www.ilograph.com/docs/editing/perspectives/references/#aliases" 167 | ); 168 | 169 | const stepSchema = z 170 | .object({ 171 | to: z 172 | .string() 173 | .describe( 174 | "The identifier of the resource this step of the sequence is to" 175 | ) 176 | .optional(), 177 | toAndBack: z 178 | .string() 179 | .describe( 180 | "The identifier of the resource this step of the sequence is to. A second step back to the previous resource is automatically added" 181 | ) 182 | .optional(), 183 | toAsync: z 184 | .string() 185 | .describe( 186 | "The identifier of the resource this step of the sequence is to. Unlike with to, however, control is not passed to the new step. Async steps are drawn with dashed arrows" 187 | ) 188 | .optional(), 189 | restartAt: z 190 | .string() 191 | .describe( 192 | "The identifier of the resource to pass control to without drawing an arrow. The next step will originate from this resource" 193 | ) 194 | .optional(), 195 | label: z 196 | .string() 197 | .describe("The label that appears above the arrow of this step") 198 | .optional(), 199 | description: z 200 | .string() 201 | .describe( 202 | "The extended description that appears when the user’s mouse hovers over the arrow of this step. Has support for markdown, and can contain multiple lines." 203 | ) 204 | .optional(), 205 | bidirectional: z 206 | .boolean() 207 | .describe( 208 | "If true, arrow(s) for this step are show with arrowheads on both ends. Defaults to false." 209 | ) 210 | .default(false), 211 | color: z 212 | .string() 213 | .describe( 214 | "The arrow and text color of the step. Can be any X11 color name or hex (e.g. #FF00FF). Defaults to #303030. This color is inverted when dark mode is enabled by the user" 215 | ) 216 | .optional() 217 | .default("#303030"), 218 | }) 219 | .strict(); 220 | 221 | const sequenceSchema = z 222 | .object({ 223 | start: z 224 | .string() 225 | .describe("The identifier of the starting resource in the sequence"), 226 | steps: z.array(stepSchema).optional(), 227 | }) 228 | .strict() 229 | .describe( 230 | "When sequence is set in a perspective, the sequence defines how resources are related in a sequence of steps in the perspective. Perspectives with sequence defined are sequence perspectives." 231 | ); 232 | 233 | const relationSchema = z 234 | .object({ 235 | from: z 236 | .string() 237 | .describe( 238 | "The identifier (or comma-separated identifiers) of the dependent (left-side) resource(s) in this relation. Can refer to resource ids or alias ids" 239 | ) 240 | .optional(), 241 | to: z 242 | .string() 243 | .describe( 244 | "The identifier (or comma-separated identifiers) of the independent (right-side) resource(s) in this relation. Can refer to resource ids or alias ids" 245 | ) 246 | .optional(), 247 | label: z 248 | .string() 249 | .describe("The label that appears above the arrow(s) in this relation") 250 | .optional(), 251 | description: z 252 | .string() 253 | .describe( 254 | "The extended description that appears when the user’s mouse hovers over the arrow(s) in this relation. Has support for markdown, and can contain multiple lines." 255 | ) 256 | .optional(), 257 | color: z 258 | .string() 259 | .describe( 260 | "The arrow and text color of the relation. Can be any X11 color name or hex (e.g. #FF00FF). Defaults to #303030. This color is inverted when dark mode is enabled by the user" 261 | ) 262 | .default("#303030"), 263 | secondary: z 264 | .boolean() 265 | .describe( 266 | "If true, the relation will not affect the perspective’s layout. Does not affect ring perspectives. Defaults to false." 267 | ) 268 | .default(false), 269 | }) 270 | .strict(); 271 | 272 | const perspectiveSchema = z 273 | .object({ 274 | id: z 275 | .string() 276 | .describe( 277 | "A unique identifier for the perspective. If not provided, the name property is used as the perspective identifer" 278 | ) 279 | .optional(), 280 | name: z 281 | .string() 282 | .describe( 283 | "The name of the perspective. Used as the unique identifer of the perspective if id is not provided" 284 | ), 285 | notes: z 286 | .string() 287 | .describe( 288 | "Notes that appear in the notes panel when viewing the perspective, and in the overview page. Should be used to describe the perspective. Has support for markdown, and can contain multiple lines." 289 | ) 290 | .optional(), 291 | color: z 292 | .string() 293 | .describe( 294 | "The color of the perspective. Can be any X11 color name or hex (e.g. “#FF00FF”). Defaults to royalblue" 295 | ) 296 | .optional() 297 | .default("royalblue"), 298 | extends: z 299 | .string() 300 | .describe( 301 | "The previously-defined perspective (or comma-separated perspectives) that this perspective extends. When specifying multiple perspectives to extend, they are applied in the order they are specified. Values from perspectives listed later take precedence." 302 | ) 303 | .optional(), 304 | relations: z.array(relationSchema).optional(), 305 | sequence: sequenceSchema.optional(), 306 | aliases: z.array(aliasSchema).optional(), 307 | overrides: z.array(overrideSchema).optional(), 308 | walkthrough: z.array(slideSchema).optional(), 309 | defaultArrowLabel: z 310 | .string() 311 | .describe( 312 | "The default label for relation/step arrows that don’t have a label specified" 313 | ) 314 | .optional(), 315 | defaultArrowColor: z 316 | .string() 317 | .describe( 318 | "The default color for relation/step arrows that don’t have a color specified" 319 | ) 320 | .optional(), 321 | arrowDirection: arrowDirectionSchema.describe( 322 | "The default arrow direction for relations. The direction specified is relative to the perspective’s orientation. Individual relations can override this value by specifying their own relation value. Accepted values are forward, backward, and bidirectional. Defaults to forward. Does not affect sequence perspectives" 323 | ), 324 | orientation: z 325 | .enum(["leftToRight", "topToBottom", "ring"]) 326 | .describe( 327 | "Which direction the perspective is oriented. Accepted values are leftToRight, topToBottom, and ring. Defaults to leftToRight. Does not affect sequence perspectives" 328 | ) 329 | .optional() 330 | .default("leftToRight"), 331 | additionalContext: z 332 | .enum(["all", "none", "super-only", "sub-only"]) 333 | .describe( 334 | "What additional context, if any, to show in this perspective. Accepted values are all, none, super-only and sub-only. Defaults to super-only. Read more about controlling context https://www.ilograph.com/docs/editing/perspectives/other-properties/" 335 | ) 336 | .optional() 337 | .default("super-only"), 338 | unwrapContext: z 339 | .boolean() 340 | .describe( 341 | "If true, context resources may be ommited to ensure that relation arrows always flow in the specified arrowDirection. If false, resources always appear in their parent context resource, and arrow directions will automatically reverse to accomodate this. Defaults to false. Does not affect sequence or ring perspectives" 342 | ) 343 | .optional() 344 | .default(false), 345 | deduplicateImportedResources: z 346 | .boolean() 347 | .describe( 348 | "If true, resources that are imported multiple times will be de-duplicated in this perspective. If false, every imported instance of a resource will appear as a unique resource. Defaults to true." 349 | ) 350 | .optional() 351 | .default(true), 352 | hidden: z 353 | .boolean() 354 | .describe( 355 | "If true, resources that are imported multiple times will be de-duplicated in this perspective. If false, every imported instance of a resource will appear as a unique resource. Defaults to true." 356 | ) 357 | .optional() 358 | .default(false), 359 | }) 360 | .strict(); 361 | 362 | const resourceBaseSchema = z 363 | .object({ 364 | name: z 365 | .string() 366 | .describe( 367 | "The name of the resource. Used as the identifer of the resource if id is not provided. If name contains a restricted character (/, ^, *, [, ], or ,), id must be defined." 368 | ), 369 | subtitle: z 370 | .string() 371 | .describe( 372 | "The subtitle of the resource (appears below the resource name)" 373 | ) 374 | .optional(), 375 | description: z 376 | .union([z.string(), z.record(z.string(), z.string())]) 377 | .describe( 378 | "The description of the resource (appears below the resource subtitle when focused). Has support for markdown. Descriptions can contain multiple lines or be defined as key-value pairs." 379 | ) 380 | .optional(), 381 | color: z 382 | .string() 383 | .describe( 384 | "The text color of the resource. Can be any X11 color name or hex (e.g. #FF00FF). Defaults to dimgray. This color is inverted when dark mode is enabled by the user" 385 | ) 386 | .optional(), 387 | // .default("dimgray"), 388 | backgroundColor: z 389 | .string() 390 | .describe( 391 | "The background color of the resource. Can be any X11 color name or hex (e.g. #FF00FF). Defaults to white. This color is inverted when dark mode is enabled by the user" 392 | ) 393 | .optional(), 394 | // .default("white"), 395 | style: z 396 | .enum(["default", "plural", "dashed", "outline", "flat"]) 397 | .describe( 398 | "The resource border style. When set to plural, the resource is rendered as multiple boxes. When set to dashed, the resource border is rendered as a dashed line. When set to outline, the resource border is rendered as a solid line. When set to flat, no border is rendered. Accepted values are default, plural, dashed, outline, and flat. Defaults to default" 399 | ) 400 | .optional(), 401 | // .default("default"), 402 | abstract: z 403 | .boolean() 404 | .describe( 405 | "When set to true, other resources may inherit from this resource using instanceOf (see below). Abstract resources cannot be referenced directly in perspectives" 406 | ) 407 | .optional(), 408 | instanceOf: z 409 | .string() 410 | .describe( 411 | "When specified, this resource inherits all properties (other than id/name) and child resources of the specified abstract resource. Any additional properties specified, including children, will override the inherited property value" 412 | ) 413 | .optional(), 414 | 415 | icon: z.string().describe("An icon path").optional(), 416 | iconStyle: z 417 | .enum(["default", "silhouette"]) 418 | .describe( 419 | "Controls how the icon is rendered. If set to default the icon is rendered normally. If set to silhouette, the icon is rendered as an outline in the same color as the resource text. Defaults to default" 420 | ) 421 | .optional(), 422 | url: z 423 | .string() 424 | .describe( 425 | "A URL for the resource. If defined, a “link” icon linking to the URL appears in the resource when it is selected" 426 | ) 427 | .optional(), 428 | layout: layoutSchema.optional(), 429 | id: z 430 | .string() 431 | .describe( 432 | "An substitute identifier for the resource. Cannot contain /, ^, *, [, ], or , characters" 433 | ) 434 | .optional(), 435 | }) 436 | .strict(); 437 | 438 | type Resource = z.infer & { 439 | children?: Resource[]; 440 | }; 441 | 442 | // Note: defaults do not work with recursive types 443 | const resourceSchema: z.ZodType = resourceBaseSchema 444 | .extend({ 445 | children: z.lazy(() => resourceSchema.array()).optional(), 446 | }) 447 | .strict(); 448 | 449 | export const ilographSchema = z 450 | .object({ 451 | resources: z.array(resourceSchema).optional(), 452 | perspectives: z.array(perspectiveSchema).optional(), 453 | imports: z.array(importSchema).optional(), 454 | contexts: z.array(contextSchema).optional(), 455 | description: z 456 | .string() 457 | .describe( 458 | "A description of the diagram that appears at the top of the overview page. Has support for markdown, and can contain multiple lines." 459 | ) 460 | .optional(), 461 | defaultContextDisplayName: z 462 | .string() 463 | .describe("The display name of the default context") 464 | .optional(), 465 | }) 466 | .strict(); 467 | 468 | export type Ilograph = z.infer; 469 | 470 | // import { writeFile } from "fs/promises"; 471 | // import { zodToJsonSchema } from "zod-to-json-schema"; 472 | // await writeFile( 473 | // "experiment/ilograph.json", 474 | // JSON.stringify(zodToJsonSchema(ilographSchema), null, 2) 475 | // ); 476 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | '@hpcc-js/wasm-graphviz': 12 | specifier: ^1.7.0 13 | version: 1.7.0 14 | mnemonist: 15 | specifier: ^0.40.3 16 | version: 0.40.3 17 | ts-graphviz: 18 | specifier: ^2.1.6 19 | version: 2.1.6 20 | yaml: 21 | specifier: ^2.7.1 22 | version: 2.8.0 23 | zod: 24 | specifier: 3.24.1 25 | version: 3.24.1 26 | devDependencies: 27 | '@resvg/resvg-js': 28 | specifier: ^2.6.2 29 | version: 2.6.2 30 | '@types/node': 31 | specifier: ^22.14.1 32 | version: 22.15.18 33 | jest-image-snapshot: 34 | specifier: ^6.4.0 35 | version: 6.5.0 36 | typescript: 37 | specifier: ^5.8.3 38 | version: 5.8.3 39 | vitest: 40 | specifier: ^3.1.2 41 | version: 3.1.3(@types/node@22.15.18)(yaml@2.8.0) 42 | zod-to-json-schema: 43 | specifier: ^3.24.5 44 | version: 3.24.5(zod@3.24.1) 45 | 46 | packages: 47 | 48 | '@esbuild/aix-ppc64@0.25.3': 49 | resolution: {integrity: sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==} 50 | engines: {node: '>=18'} 51 | cpu: [ppc64] 52 | os: [aix] 53 | 54 | '@esbuild/android-arm64@0.25.3': 55 | resolution: {integrity: sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==} 56 | engines: {node: '>=18'} 57 | cpu: [arm64] 58 | os: [android] 59 | 60 | '@esbuild/android-arm@0.25.3': 61 | resolution: {integrity: sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==} 62 | engines: {node: '>=18'} 63 | cpu: [arm] 64 | os: [android] 65 | 66 | '@esbuild/android-x64@0.25.3': 67 | resolution: {integrity: sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==} 68 | engines: {node: '>=18'} 69 | cpu: [x64] 70 | os: [android] 71 | 72 | '@esbuild/darwin-arm64@0.25.3': 73 | resolution: {integrity: sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==} 74 | engines: {node: '>=18'} 75 | cpu: [arm64] 76 | os: [darwin] 77 | 78 | '@esbuild/darwin-x64@0.25.3': 79 | resolution: {integrity: sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==} 80 | engines: {node: '>=18'} 81 | cpu: [x64] 82 | os: [darwin] 83 | 84 | '@esbuild/freebsd-arm64@0.25.3': 85 | resolution: {integrity: sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==} 86 | engines: {node: '>=18'} 87 | cpu: [arm64] 88 | os: [freebsd] 89 | 90 | '@esbuild/freebsd-x64@0.25.3': 91 | resolution: {integrity: sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==} 92 | engines: {node: '>=18'} 93 | cpu: [x64] 94 | os: [freebsd] 95 | 96 | '@esbuild/linux-arm64@0.25.3': 97 | resolution: {integrity: sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==} 98 | engines: {node: '>=18'} 99 | cpu: [arm64] 100 | os: [linux] 101 | 102 | '@esbuild/linux-arm@0.25.3': 103 | resolution: {integrity: sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==} 104 | engines: {node: '>=18'} 105 | cpu: [arm] 106 | os: [linux] 107 | 108 | '@esbuild/linux-ia32@0.25.3': 109 | resolution: {integrity: sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==} 110 | engines: {node: '>=18'} 111 | cpu: [ia32] 112 | os: [linux] 113 | 114 | '@esbuild/linux-loong64@0.25.3': 115 | resolution: {integrity: sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==} 116 | engines: {node: '>=18'} 117 | cpu: [loong64] 118 | os: [linux] 119 | 120 | '@esbuild/linux-mips64el@0.25.3': 121 | resolution: {integrity: sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==} 122 | engines: {node: '>=18'} 123 | cpu: [mips64el] 124 | os: [linux] 125 | 126 | '@esbuild/linux-ppc64@0.25.3': 127 | resolution: {integrity: sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==} 128 | engines: {node: '>=18'} 129 | cpu: [ppc64] 130 | os: [linux] 131 | 132 | '@esbuild/linux-riscv64@0.25.3': 133 | resolution: {integrity: sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==} 134 | engines: {node: '>=18'} 135 | cpu: [riscv64] 136 | os: [linux] 137 | 138 | '@esbuild/linux-s390x@0.25.3': 139 | resolution: {integrity: sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==} 140 | engines: {node: '>=18'} 141 | cpu: [s390x] 142 | os: [linux] 143 | 144 | '@esbuild/linux-x64@0.25.3': 145 | resolution: {integrity: sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==} 146 | engines: {node: '>=18'} 147 | cpu: [x64] 148 | os: [linux] 149 | 150 | '@esbuild/netbsd-arm64@0.25.3': 151 | resolution: {integrity: sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==} 152 | engines: {node: '>=18'} 153 | cpu: [arm64] 154 | os: [netbsd] 155 | 156 | '@esbuild/netbsd-x64@0.25.3': 157 | resolution: {integrity: sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==} 158 | engines: {node: '>=18'} 159 | cpu: [x64] 160 | os: [netbsd] 161 | 162 | '@esbuild/openbsd-arm64@0.25.3': 163 | resolution: {integrity: sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==} 164 | engines: {node: '>=18'} 165 | cpu: [arm64] 166 | os: [openbsd] 167 | 168 | '@esbuild/openbsd-x64@0.25.3': 169 | resolution: {integrity: sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==} 170 | engines: {node: '>=18'} 171 | cpu: [x64] 172 | os: [openbsd] 173 | 174 | '@esbuild/sunos-x64@0.25.3': 175 | resolution: {integrity: sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==} 176 | engines: {node: '>=18'} 177 | cpu: [x64] 178 | os: [sunos] 179 | 180 | '@esbuild/win32-arm64@0.25.3': 181 | resolution: {integrity: sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==} 182 | engines: {node: '>=18'} 183 | cpu: [arm64] 184 | os: [win32] 185 | 186 | '@esbuild/win32-ia32@0.25.3': 187 | resolution: {integrity: sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==} 188 | engines: {node: '>=18'} 189 | cpu: [ia32] 190 | os: [win32] 191 | 192 | '@esbuild/win32-x64@0.25.3': 193 | resolution: {integrity: sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==} 194 | engines: {node: '>=18'} 195 | cpu: [x64] 196 | os: [win32] 197 | 198 | '@hpcc-js/wasm-graphviz@1.7.0': 199 | resolution: {integrity: sha512-/1XEmubfDz1UolKiVh6H4BrGX0DZwR39Y2h76LdqR17FZ3gCJoUs4jV7kUywedXerbwx1szhU3dBsNcvkEmjiQ==} 200 | 201 | '@jridgewell/sourcemap-codec@1.5.0': 202 | resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 203 | 204 | '@resvg/resvg-js-android-arm-eabi@2.6.2': 205 | resolution: {integrity: sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==} 206 | engines: {node: '>= 10'} 207 | cpu: [arm] 208 | os: [android] 209 | 210 | '@resvg/resvg-js-android-arm64@2.6.2': 211 | resolution: {integrity: sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==} 212 | engines: {node: '>= 10'} 213 | cpu: [arm64] 214 | os: [android] 215 | 216 | '@resvg/resvg-js-darwin-arm64@2.6.2': 217 | resolution: {integrity: sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==} 218 | engines: {node: '>= 10'} 219 | cpu: [arm64] 220 | os: [darwin] 221 | 222 | '@resvg/resvg-js-darwin-x64@2.6.2': 223 | resolution: {integrity: sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==} 224 | engines: {node: '>= 10'} 225 | cpu: [x64] 226 | os: [darwin] 227 | 228 | '@resvg/resvg-js-linux-arm-gnueabihf@2.6.2': 229 | resolution: {integrity: sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==} 230 | engines: {node: '>= 10'} 231 | cpu: [arm] 232 | os: [linux] 233 | 234 | '@resvg/resvg-js-linux-arm64-gnu@2.6.2': 235 | resolution: {integrity: sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==} 236 | engines: {node: '>= 10'} 237 | cpu: [arm64] 238 | os: [linux] 239 | 240 | '@resvg/resvg-js-linux-arm64-musl@2.6.2': 241 | resolution: {integrity: sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==} 242 | engines: {node: '>= 10'} 243 | cpu: [arm64] 244 | os: [linux] 245 | 246 | '@resvg/resvg-js-linux-x64-gnu@2.6.2': 247 | resolution: {integrity: sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==} 248 | engines: {node: '>= 10'} 249 | cpu: [x64] 250 | os: [linux] 251 | 252 | '@resvg/resvg-js-linux-x64-musl@2.6.2': 253 | resolution: {integrity: sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==} 254 | engines: {node: '>= 10'} 255 | cpu: [x64] 256 | os: [linux] 257 | 258 | '@resvg/resvg-js-win32-arm64-msvc@2.6.2': 259 | resolution: {integrity: sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==} 260 | engines: {node: '>= 10'} 261 | cpu: [arm64] 262 | os: [win32] 263 | 264 | '@resvg/resvg-js-win32-ia32-msvc@2.6.2': 265 | resolution: {integrity: sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==} 266 | engines: {node: '>= 10'} 267 | cpu: [ia32] 268 | os: [win32] 269 | 270 | '@resvg/resvg-js-win32-x64-msvc@2.6.2': 271 | resolution: {integrity: sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==} 272 | engines: {node: '>= 10'} 273 | cpu: [x64] 274 | os: [win32] 275 | 276 | '@resvg/resvg-js@2.6.2': 277 | resolution: {integrity: sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==} 278 | engines: {node: '>= 10'} 279 | 280 | '@rollup/rollup-android-arm-eabi@4.40.0': 281 | resolution: {integrity: sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==} 282 | cpu: [arm] 283 | os: [android] 284 | 285 | '@rollup/rollup-android-arm64@4.40.0': 286 | resolution: {integrity: sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==} 287 | cpu: [arm64] 288 | os: [android] 289 | 290 | '@rollup/rollup-darwin-arm64@4.40.0': 291 | resolution: {integrity: sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==} 292 | cpu: [arm64] 293 | os: [darwin] 294 | 295 | '@rollup/rollup-darwin-x64@4.40.0': 296 | resolution: {integrity: sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==} 297 | cpu: [x64] 298 | os: [darwin] 299 | 300 | '@rollup/rollup-freebsd-arm64@4.40.0': 301 | resolution: {integrity: sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==} 302 | cpu: [arm64] 303 | os: [freebsd] 304 | 305 | '@rollup/rollup-freebsd-x64@4.40.0': 306 | resolution: {integrity: sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==} 307 | cpu: [x64] 308 | os: [freebsd] 309 | 310 | '@rollup/rollup-linux-arm-gnueabihf@4.40.0': 311 | resolution: {integrity: sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==} 312 | cpu: [arm] 313 | os: [linux] 314 | 315 | '@rollup/rollup-linux-arm-musleabihf@4.40.0': 316 | resolution: {integrity: sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==} 317 | cpu: [arm] 318 | os: [linux] 319 | 320 | '@rollup/rollup-linux-arm64-gnu@4.40.0': 321 | resolution: {integrity: sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==} 322 | cpu: [arm64] 323 | os: [linux] 324 | 325 | '@rollup/rollup-linux-arm64-musl@4.40.0': 326 | resolution: {integrity: sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==} 327 | cpu: [arm64] 328 | os: [linux] 329 | 330 | '@rollup/rollup-linux-loongarch64-gnu@4.40.0': 331 | resolution: {integrity: sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==} 332 | cpu: [loong64] 333 | os: [linux] 334 | 335 | '@rollup/rollup-linux-powerpc64le-gnu@4.40.0': 336 | resolution: {integrity: sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==} 337 | cpu: [ppc64] 338 | os: [linux] 339 | 340 | '@rollup/rollup-linux-riscv64-gnu@4.40.0': 341 | resolution: {integrity: sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==} 342 | cpu: [riscv64] 343 | os: [linux] 344 | 345 | '@rollup/rollup-linux-riscv64-musl@4.40.0': 346 | resolution: {integrity: sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==} 347 | cpu: [riscv64] 348 | os: [linux] 349 | 350 | '@rollup/rollup-linux-s390x-gnu@4.40.0': 351 | resolution: {integrity: sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==} 352 | cpu: [s390x] 353 | os: [linux] 354 | 355 | '@rollup/rollup-linux-x64-gnu@4.40.0': 356 | resolution: {integrity: sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==} 357 | cpu: [x64] 358 | os: [linux] 359 | 360 | '@rollup/rollup-linux-x64-musl@4.40.0': 361 | resolution: {integrity: sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==} 362 | cpu: [x64] 363 | os: [linux] 364 | 365 | '@rollup/rollup-win32-arm64-msvc@4.40.0': 366 | resolution: {integrity: sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==} 367 | cpu: [arm64] 368 | os: [win32] 369 | 370 | '@rollup/rollup-win32-ia32-msvc@4.40.0': 371 | resolution: {integrity: sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==} 372 | cpu: [ia32] 373 | os: [win32] 374 | 375 | '@rollup/rollup-win32-x64-msvc@4.40.0': 376 | resolution: {integrity: sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==} 377 | cpu: [x64] 378 | os: [win32] 379 | 380 | '@ts-graphviz/adapter@2.0.6': 381 | resolution: {integrity: sha512-kJ10lIMSWMJkLkkCG5gt927SnGZcBuG0s0HHswGzcHTgvtUe7yk5/3zTEr0bafzsodsOq5Gi6FhQeV775nC35Q==} 382 | engines: {node: '>=18'} 383 | 384 | '@ts-graphviz/ast@2.0.7': 385 | resolution: {integrity: sha512-e6+2qtNV99UT6DJSoLbHfkzfyqY84aIuoV8Xlb9+hZAjgpum8iVHprGeAMQ4rF6sKUAxrmY8rfF/vgAwoPc3gw==} 386 | engines: {node: '>=18'} 387 | 388 | '@ts-graphviz/common@2.1.5': 389 | resolution: {integrity: sha512-S6/9+T6x8j6cr/gNhp+U2olwo1n0jKj/682QVqsh7yXWV6ednHYqxFw0ZsY3LyzT0N8jaZ6jQY9YD99le3cmvg==} 390 | engines: {node: '>=18'} 391 | 392 | '@ts-graphviz/core@2.0.7': 393 | resolution: {integrity: sha512-w071DSzP94YfN6XiWhOxnLpYT3uqtxJBDYdh6Jdjzt+Ce6DNspJsPQgpC7rbts/B8tEkq0LHoYuIF/O5Jh5rPg==} 394 | engines: {node: '>=18'} 395 | 396 | '@types/estree@1.0.7': 397 | resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} 398 | 399 | '@types/node@22.15.18': 400 | resolution: {integrity: sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==} 401 | 402 | '@vitest/expect@3.1.3': 403 | resolution: {integrity: sha512-7FTQQuuLKmN1Ig/h+h/GO+44Q1IlglPlR2es4ab7Yvfx+Uk5xsv+Ykk+MEt/M2Yn/xGmzaLKxGw2lgy2bwuYqg==} 404 | 405 | '@vitest/mocker@3.1.3': 406 | resolution: {integrity: sha512-PJbLjonJK82uCWHjzgBJZuR7zmAOrSvKk1QBxrennDIgtH4uK0TB1PvYmc0XBCigxxtiAVPfWtAdy4lpz8SQGQ==} 407 | peerDependencies: 408 | msw: ^2.4.9 409 | vite: ^5.0.0 || ^6.0.0 410 | peerDependenciesMeta: 411 | msw: 412 | optional: true 413 | vite: 414 | optional: true 415 | 416 | '@vitest/pretty-format@3.1.3': 417 | resolution: {integrity: sha512-i6FDiBeJUGLDKADw2Gb01UtUNb12yyXAqC/mmRWuYl+m/U9GS7s8us5ONmGkGpUUo7/iAYzI2ePVfOZTYvUifA==} 418 | 419 | '@vitest/runner@3.1.3': 420 | resolution: {integrity: sha512-Tae+ogtlNfFei5DggOsSUvkIaSuVywujMj6HzR97AHK6XK8i3BuVyIifWAm/sE3a15lF5RH9yQIrbXYuo0IFyA==} 421 | 422 | '@vitest/snapshot@3.1.3': 423 | resolution: {integrity: sha512-XVa5OPNTYUsyqG9skuUkFzAeFnEzDp8hQu7kZ0N25B1+6KjGm4hWLtURyBbsIAOekfWQ7Wuz/N/XXzgYO3deWQ==} 424 | 425 | '@vitest/spy@3.1.3': 426 | resolution: {integrity: sha512-x6w+ctOEmEXdWaa6TO4ilb7l9DxPR5bwEb6hILKuxfU1NqWT2mpJD9NJN7t3OTfxmVlOMrvtoFJGdgyzZ605lQ==} 427 | 428 | '@vitest/utils@3.1.3': 429 | resolution: {integrity: sha512-2Ltrpht4OmHO9+c/nmHtF09HWiyWdworqnHIwjfvDyWjuwKbdkcS9AnhsDn+8E2RM4x++foD1/tNuLPVvWG1Rg==} 430 | 431 | ansi-styles@4.3.0: 432 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 433 | engines: {node: '>=8'} 434 | 435 | assertion-error@2.0.1: 436 | resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} 437 | engines: {node: '>=12'} 438 | 439 | cac@6.7.14: 440 | resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} 441 | engines: {node: '>=8'} 442 | 443 | chai@5.2.0: 444 | resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} 445 | engines: {node: '>=12'} 446 | 447 | chalk@4.1.2: 448 | resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 449 | engines: {node: '>=10'} 450 | 451 | check-error@2.1.1: 452 | resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} 453 | engines: {node: '>= 16'} 454 | 455 | color-convert@2.0.1: 456 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 457 | engines: {node: '>=7.0.0'} 458 | 459 | color-name@1.1.4: 460 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 461 | 462 | debug@4.4.0: 463 | resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} 464 | engines: {node: '>=6.0'} 465 | peerDependencies: 466 | supports-color: '*' 467 | peerDependenciesMeta: 468 | supports-color: 469 | optional: true 470 | 471 | deep-eql@5.0.2: 472 | resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} 473 | engines: {node: '>=6'} 474 | 475 | es-module-lexer@1.7.0: 476 | resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} 477 | 478 | esbuild@0.25.3: 479 | resolution: {integrity: sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==} 480 | engines: {node: '>=18'} 481 | hasBin: true 482 | 483 | estree-walker@3.0.3: 484 | resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} 485 | 486 | expect-type@1.2.1: 487 | resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==} 488 | engines: {node: '>=12.0.0'} 489 | 490 | fdir@6.4.4: 491 | resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==} 492 | peerDependencies: 493 | picomatch: ^3 || ^4 494 | peerDependenciesMeta: 495 | picomatch: 496 | optional: true 497 | 498 | fsevents@2.3.3: 499 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 500 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 501 | os: [darwin] 502 | 503 | get-stdin@5.0.1: 504 | resolution: {integrity: sha512-jZV7n6jGE3Gt7fgSTJoz91Ak5MuTLwMwkoYdjxuJ/AmjIsE1UC03y/IWkZCQGEvVNS9qoRNwy5BCqxImv0FVeA==} 505 | engines: {node: '>=0.12.0'} 506 | 507 | glur@1.1.2: 508 | resolution: {integrity: sha512-l+8esYHTKOx2G/Aao4lEQ0bnHWg4fWtJbVoZZT9Knxi01pB8C80BR85nONLFwkkQoFRCmXY+BUcGZN3yZ2QsRA==} 509 | 510 | has-flag@4.0.0: 511 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 512 | engines: {node: '>=8'} 513 | 514 | jest-image-snapshot@6.5.0: 515 | resolution: {integrity: sha512-oTMGQEpvW3Q3+BwlULINBJRpvsVDXAILqjniCuiUT7us4f1YDQF99fPrMVy4yIQui1ZIHe7fjqRsErfK6tUlSQ==} 516 | engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} 517 | peerDependencies: 518 | jest: '>=20 <=29' 519 | peerDependenciesMeta: 520 | jest: 521 | optional: true 522 | 523 | lodash@4.17.21: 524 | resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} 525 | 526 | loupe@3.1.3: 527 | resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} 528 | 529 | magic-string@0.30.17: 530 | resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} 531 | 532 | mnemonist@0.40.3: 533 | resolution: {integrity: sha512-Vjyr90sJ23CKKH/qPAgUKicw/v6pRoamxIEDFOF8uSgFME7DqPRpHgRTejWVjkdGg5dXj0/NyxZHZ9bcjH+2uQ==} 534 | 535 | ms@2.1.3: 536 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 537 | 538 | nanoid@3.3.11: 539 | resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} 540 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 541 | hasBin: true 542 | 543 | obliterator@2.0.5: 544 | resolution: {integrity: sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==} 545 | 546 | pathe@2.0.3: 547 | resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} 548 | 549 | pathval@2.0.0: 550 | resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} 551 | engines: {node: '>= 14.16'} 552 | 553 | picocolors@1.1.1: 554 | resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 555 | 556 | picomatch@4.0.2: 557 | resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} 558 | engines: {node: '>=12'} 559 | 560 | pixelmatch@5.3.0: 561 | resolution: {integrity: sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==} 562 | hasBin: true 563 | 564 | pngjs@3.4.0: 565 | resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==} 566 | engines: {node: '>=4.0.0'} 567 | 568 | pngjs@6.0.0: 569 | resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==} 570 | engines: {node: '>=12.13.0'} 571 | 572 | postcss@8.5.3: 573 | resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} 574 | engines: {node: ^10 || ^12 || >=14} 575 | 576 | rollup@4.40.0: 577 | resolution: {integrity: sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==} 578 | engines: {node: '>=18.0.0', npm: '>=8.0.0'} 579 | hasBin: true 580 | 581 | siginfo@2.0.0: 582 | resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} 583 | 584 | source-map-js@1.2.1: 585 | resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 586 | engines: {node: '>=0.10.0'} 587 | 588 | ssim.js@3.5.0: 589 | resolution: {integrity: sha512-Aj6Jl2z6oDmgYFFbQqK7fght19bXdOxY7Tj03nF+03M9gCBAjeIiO8/PlEGMfKDwYpw4q6iBqVq2YuREorGg/g==} 590 | 591 | stackback@0.0.2: 592 | resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} 593 | 594 | std-env@3.9.0: 595 | resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} 596 | 597 | supports-color@7.2.0: 598 | resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 599 | engines: {node: '>=8'} 600 | 601 | tinybench@2.9.0: 602 | resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} 603 | 604 | tinyexec@0.3.2: 605 | resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} 606 | 607 | tinyglobby@0.2.13: 608 | resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} 609 | engines: {node: '>=12.0.0'} 610 | 611 | tinypool@1.0.2: 612 | resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} 613 | engines: {node: ^18.0.0 || >=20.0.0} 614 | 615 | tinyrainbow@2.0.0: 616 | resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} 617 | engines: {node: '>=14.0.0'} 618 | 619 | tinyspy@3.0.2: 620 | resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} 621 | engines: {node: '>=14.0.0'} 622 | 623 | ts-graphviz@2.1.6: 624 | resolution: {integrity: sha512-XyLVuhBVvdJTJr2FJJV2L1pc4MwSjMhcunRVgDE9k4wbb2ee7ORYnPewxMWUav12vxyfUM686MSGsqnVRIInuw==} 625 | engines: {node: '>=18'} 626 | 627 | typescript@5.8.3: 628 | resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} 629 | engines: {node: '>=14.17'} 630 | hasBin: true 631 | 632 | undici-types@6.21.0: 633 | resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} 634 | 635 | vite-node@3.1.3: 636 | resolution: {integrity: sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA==} 637 | engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} 638 | hasBin: true 639 | 640 | vite@6.3.3: 641 | resolution: {integrity: sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==} 642 | engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} 643 | hasBin: true 644 | peerDependencies: 645 | '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 646 | jiti: '>=1.21.0' 647 | less: '*' 648 | lightningcss: ^1.21.0 649 | sass: '*' 650 | sass-embedded: '*' 651 | stylus: '*' 652 | sugarss: '*' 653 | terser: ^5.16.0 654 | tsx: ^4.8.1 655 | yaml: ^2.4.2 656 | peerDependenciesMeta: 657 | '@types/node': 658 | optional: true 659 | jiti: 660 | optional: true 661 | less: 662 | optional: true 663 | lightningcss: 664 | optional: true 665 | sass: 666 | optional: true 667 | sass-embedded: 668 | optional: true 669 | stylus: 670 | optional: true 671 | sugarss: 672 | optional: true 673 | terser: 674 | optional: true 675 | tsx: 676 | optional: true 677 | yaml: 678 | optional: true 679 | 680 | vitest@3.1.3: 681 | resolution: {integrity: sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw==} 682 | engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} 683 | hasBin: true 684 | peerDependencies: 685 | '@edge-runtime/vm': '*' 686 | '@types/debug': ^4.1.12 687 | '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 688 | '@vitest/browser': 3.1.3 689 | '@vitest/ui': 3.1.3 690 | happy-dom: '*' 691 | jsdom: '*' 692 | peerDependenciesMeta: 693 | '@edge-runtime/vm': 694 | optional: true 695 | '@types/debug': 696 | optional: true 697 | '@types/node': 698 | optional: true 699 | '@vitest/browser': 700 | optional: true 701 | '@vitest/ui': 702 | optional: true 703 | happy-dom: 704 | optional: true 705 | jsdom: 706 | optional: true 707 | 708 | why-is-node-running@2.3.0: 709 | resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} 710 | engines: {node: '>=8'} 711 | hasBin: true 712 | 713 | yaml@2.8.0: 714 | resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} 715 | engines: {node: '>= 14.6'} 716 | hasBin: true 717 | 718 | zod-to-json-schema@3.24.5: 719 | resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==} 720 | peerDependencies: 721 | zod: ^3.24.1 722 | 723 | zod@3.24.1: 724 | resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} 725 | 726 | snapshots: 727 | 728 | '@esbuild/aix-ppc64@0.25.3': 729 | optional: true 730 | 731 | '@esbuild/android-arm64@0.25.3': 732 | optional: true 733 | 734 | '@esbuild/android-arm@0.25.3': 735 | optional: true 736 | 737 | '@esbuild/android-x64@0.25.3': 738 | optional: true 739 | 740 | '@esbuild/darwin-arm64@0.25.3': 741 | optional: true 742 | 743 | '@esbuild/darwin-x64@0.25.3': 744 | optional: true 745 | 746 | '@esbuild/freebsd-arm64@0.25.3': 747 | optional: true 748 | 749 | '@esbuild/freebsd-x64@0.25.3': 750 | optional: true 751 | 752 | '@esbuild/linux-arm64@0.25.3': 753 | optional: true 754 | 755 | '@esbuild/linux-arm@0.25.3': 756 | optional: true 757 | 758 | '@esbuild/linux-ia32@0.25.3': 759 | optional: true 760 | 761 | '@esbuild/linux-loong64@0.25.3': 762 | optional: true 763 | 764 | '@esbuild/linux-mips64el@0.25.3': 765 | optional: true 766 | 767 | '@esbuild/linux-ppc64@0.25.3': 768 | optional: true 769 | 770 | '@esbuild/linux-riscv64@0.25.3': 771 | optional: true 772 | 773 | '@esbuild/linux-s390x@0.25.3': 774 | optional: true 775 | 776 | '@esbuild/linux-x64@0.25.3': 777 | optional: true 778 | 779 | '@esbuild/netbsd-arm64@0.25.3': 780 | optional: true 781 | 782 | '@esbuild/netbsd-x64@0.25.3': 783 | optional: true 784 | 785 | '@esbuild/openbsd-arm64@0.25.3': 786 | optional: true 787 | 788 | '@esbuild/openbsd-x64@0.25.3': 789 | optional: true 790 | 791 | '@esbuild/sunos-x64@0.25.3': 792 | optional: true 793 | 794 | '@esbuild/win32-arm64@0.25.3': 795 | optional: true 796 | 797 | '@esbuild/win32-ia32@0.25.3': 798 | optional: true 799 | 800 | '@esbuild/win32-x64@0.25.3': 801 | optional: true 802 | 803 | '@hpcc-js/wasm-graphviz@1.7.0': {} 804 | 805 | '@jridgewell/sourcemap-codec@1.5.0': {} 806 | 807 | '@resvg/resvg-js-android-arm-eabi@2.6.2': 808 | optional: true 809 | 810 | '@resvg/resvg-js-android-arm64@2.6.2': 811 | optional: true 812 | 813 | '@resvg/resvg-js-darwin-arm64@2.6.2': 814 | optional: true 815 | 816 | '@resvg/resvg-js-darwin-x64@2.6.2': 817 | optional: true 818 | 819 | '@resvg/resvg-js-linux-arm-gnueabihf@2.6.2': 820 | optional: true 821 | 822 | '@resvg/resvg-js-linux-arm64-gnu@2.6.2': 823 | optional: true 824 | 825 | '@resvg/resvg-js-linux-arm64-musl@2.6.2': 826 | optional: true 827 | 828 | '@resvg/resvg-js-linux-x64-gnu@2.6.2': 829 | optional: true 830 | 831 | '@resvg/resvg-js-linux-x64-musl@2.6.2': 832 | optional: true 833 | 834 | '@resvg/resvg-js-win32-arm64-msvc@2.6.2': 835 | optional: true 836 | 837 | '@resvg/resvg-js-win32-ia32-msvc@2.6.2': 838 | optional: true 839 | 840 | '@resvg/resvg-js-win32-x64-msvc@2.6.2': 841 | optional: true 842 | 843 | '@resvg/resvg-js@2.6.2': 844 | optionalDependencies: 845 | '@resvg/resvg-js-android-arm-eabi': 2.6.2 846 | '@resvg/resvg-js-android-arm64': 2.6.2 847 | '@resvg/resvg-js-darwin-arm64': 2.6.2 848 | '@resvg/resvg-js-darwin-x64': 2.6.2 849 | '@resvg/resvg-js-linux-arm-gnueabihf': 2.6.2 850 | '@resvg/resvg-js-linux-arm64-gnu': 2.6.2 851 | '@resvg/resvg-js-linux-arm64-musl': 2.6.2 852 | '@resvg/resvg-js-linux-x64-gnu': 2.6.2 853 | '@resvg/resvg-js-linux-x64-musl': 2.6.2 854 | '@resvg/resvg-js-win32-arm64-msvc': 2.6.2 855 | '@resvg/resvg-js-win32-ia32-msvc': 2.6.2 856 | '@resvg/resvg-js-win32-x64-msvc': 2.6.2 857 | 858 | '@rollup/rollup-android-arm-eabi@4.40.0': 859 | optional: true 860 | 861 | '@rollup/rollup-android-arm64@4.40.0': 862 | optional: true 863 | 864 | '@rollup/rollup-darwin-arm64@4.40.0': 865 | optional: true 866 | 867 | '@rollup/rollup-darwin-x64@4.40.0': 868 | optional: true 869 | 870 | '@rollup/rollup-freebsd-arm64@4.40.0': 871 | optional: true 872 | 873 | '@rollup/rollup-freebsd-x64@4.40.0': 874 | optional: true 875 | 876 | '@rollup/rollup-linux-arm-gnueabihf@4.40.0': 877 | optional: true 878 | 879 | '@rollup/rollup-linux-arm-musleabihf@4.40.0': 880 | optional: true 881 | 882 | '@rollup/rollup-linux-arm64-gnu@4.40.0': 883 | optional: true 884 | 885 | '@rollup/rollup-linux-arm64-musl@4.40.0': 886 | optional: true 887 | 888 | '@rollup/rollup-linux-loongarch64-gnu@4.40.0': 889 | optional: true 890 | 891 | '@rollup/rollup-linux-powerpc64le-gnu@4.40.0': 892 | optional: true 893 | 894 | '@rollup/rollup-linux-riscv64-gnu@4.40.0': 895 | optional: true 896 | 897 | '@rollup/rollup-linux-riscv64-musl@4.40.0': 898 | optional: true 899 | 900 | '@rollup/rollup-linux-s390x-gnu@4.40.0': 901 | optional: true 902 | 903 | '@rollup/rollup-linux-x64-gnu@4.40.0': 904 | optional: true 905 | 906 | '@rollup/rollup-linux-x64-musl@4.40.0': 907 | optional: true 908 | 909 | '@rollup/rollup-win32-arm64-msvc@4.40.0': 910 | optional: true 911 | 912 | '@rollup/rollup-win32-ia32-msvc@4.40.0': 913 | optional: true 914 | 915 | '@rollup/rollup-win32-x64-msvc@4.40.0': 916 | optional: true 917 | 918 | '@ts-graphviz/adapter@2.0.6': 919 | dependencies: 920 | '@ts-graphviz/common': 2.1.5 921 | 922 | '@ts-graphviz/ast@2.0.7': 923 | dependencies: 924 | '@ts-graphviz/common': 2.1.5 925 | 926 | '@ts-graphviz/common@2.1.5': {} 927 | 928 | '@ts-graphviz/core@2.0.7': 929 | dependencies: 930 | '@ts-graphviz/ast': 2.0.7 931 | '@ts-graphviz/common': 2.1.5 932 | 933 | '@types/estree@1.0.7': {} 934 | 935 | '@types/node@22.15.18': 936 | dependencies: 937 | undici-types: 6.21.0 938 | 939 | '@vitest/expect@3.1.3': 940 | dependencies: 941 | '@vitest/spy': 3.1.3 942 | '@vitest/utils': 3.1.3 943 | chai: 5.2.0 944 | tinyrainbow: 2.0.0 945 | 946 | '@vitest/mocker@3.1.3(vite@6.3.3(@types/node@22.15.18)(yaml@2.8.0))': 947 | dependencies: 948 | '@vitest/spy': 3.1.3 949 | estree-walker: 3.0.3 950 | magic-string: 0.30.17 951 | optionalDependencies: 952 | vite: 6.3.3(@types/node@22.15.18)(yaml@2.8.0) 953 | 954 | '@vitest/pretty-format@3.1.3': 955 | dependencies: 956 | tinyrainbow: 2.0.0 957 | 958 | '@vitest/runner@3.1.3': 959 | dependencies: 960 | '@vitest/utils': 3.1.3 961 | pathe: 2.0.3 962 | 963 | '@vitest/snapshot@3.1.3': 964 | dependencies: 965 | '@vitest/pretty-format': 3.1.3 966 | magic-string: 0.30.17 967 | pathe: 2.0.3 968 | 969 | '@vitest/spy@3.1.3': 970 | dependencies: 971 | tinyspy: 3.0.2 972 | 973 | '@vitest/utils@3.1.3': 974 | dependencies: 975 | '@vitest/pretty-format': 3.1.3 976 | loupe: 3.1.3 977 | tinyrainbow: 2.0.0 978 | 979 | ansi-styles@4.3.0: 980 | dependencies: 981 | color-convert: 2.0.1 982 | 983 | assertion-error@2.0.1: {} 984 | 985 | cac@6.7.14: {} 986 | 987 | chai@5.2.0: 988 | dependencies: 989 | assertion-error: 2.0.1 990 | check-error: 2.1.1 991 | deep-eql: 5.0.2 992 | loupe: 3.1.3 993 | pathval: 2.0.0 994 | 995 | chalk@4.1.2: 996 | dependencies: 997 | ansi-styles: 4.3.0 998 | supports-color: 7.2.0 999 | 1000 | check-error@2.1.1: {} 1001 | 1002 | color-convert@2.0.1: 1003 | dependencies: 1004 | color-name: 1.1.4 1005 | 1006 | color-name@1.1.4: {} 1007 | 1008 | debug@4.4.0: 1009 | dependencies: 1010 | ms: 2.1.3 1011 | 1012 | deep-eql@5.0.2: {} 1013 | 1014 | es-module-lexer@1.7.0: {} 1015 | 1016 | esbuild@0.25.3: 1017 | optionalDependencies: 1018 | '@esbuild/aix-ppc64': 0.25.3 1019 | '@esbuild/android-arm': 0.25.3 1020 | '@esbuild/android-arm64': 0.25.3 1021 | '@esbuild/android-x64': 0.25.3 1022 | '@esbuild/darwin-arm64': 0.25.3 1023 | '@esbuild/darwin-x64': 0.25.3 1024 | '@esbuild/freebsd-arm64': 0.25.3 1025 | '@esbuild/freebsd-x64': 0.25.3 1026 | '@esbuild/linux-arm': 0.25.3 1027 | '@esbuild/linux-arm64': 0.25.3 1028 | '@esbuild/linux-ia32': 0.25.3 1029 | '@esbuild/linux-loong64': 0.25.3 1030 | '@esbuild/linux-mips64el': 0.25.3 1031 | '@esbuild/linux-ppc64': 0.25.3 1032 | '@esbuild/linux-riscv64': 0.25.3 1033 | '@esbuild/linux-s390x': 0.25.3 1034 | '@esbuild/linux-x64': 0.25.3 1035 | '@esbuild/netbsd-arm64': 0.25.3 1036 | '@esbuild/netbsd-x64': 0.25.3 1037 | '@esbuild/openbsd-arm64': 0.25.3 1038 | '@esbuild/openbsd-x64': 0.25.3 1039 | '@esbuild/sunos-x64': 0.25.3 1040 | '@esbuild/win32-arm64': 0.25.3 1041 | '@esbuild/win32-ia32': 0.25.3 1042 | '@esbuild/win32-x64': 0.25.3 1043 | 1044 | estree-walker@3.0.3: 1045 | dependencies: 1046 | '@types/estree': 1.0.7 1047 | 1048 | expect-type@1.2.1: {} 1049 | 1050 | fdir@6.4.4(picomatch@4.0.2): 1051 | optionalDependencies: 1052 | picomatch: 4.0.2 1053 | 1054 | fsevents@2.3.3: 1055 | optional: true 1056 | 1057 | get-stdin@5.0.1: {} 1058 | 1059 | glur@1.1.2: {} 1060 | 1061 | has-flag@4.0.0: {} 1062 | 1063 | jest-image-snapshot@6.5.0: 1064 | dependencies: 1065 | chalk: 4.1.2 1066 | get-stdin: 5.0.1 1067 | glur: 1.1.2 1068 | lodash: 4.17.21 1069 | pixelmatch: 5.3.0 1070 | pngjs: 3.4.0 1071 | ssim.js: 3.5.0 1072 | 1073 | lodash@4.17.21: {} 1074 | 1075 | loupe@3.1.3: {} 1076 | 1077 | magic-string@0.30.17: 1078 | dependencies: 1079 | '@jridgewell/sourcemap-codec': 1.5.0 1080 | 1081 | mnemonist@0.40.3: 1082 | dependencies: 1083 | obliterator: 2.0.5 1084 | 1085 | ms@2.1.3: {} 1086 | 1087 | nanoid@3.3.11: {} 1088 | 1089 | obliterator@2.0.5: {} 1090 | 1091 | pathe@2.0.3: {} 1092 | 1093 | pathval@2.0.0: {} 1094 | 1095 | picocolors@1.1.1: {} 1096 | 1097 | picomatch@4.0.2: {} 1098 | 1099 | pixelmatch@5.3.0: 1100 | dependencies: 1101 | pngjs: 6.0.0 1102 | 1103 | pngjs@3.4.0: {} 1104 | 1105 | pngjs@6.0.0: {} 1106 | 1107 | postcss@8.5.3: 1108 | dependencies: 1109 | nanoid: 3.3.11 1110 | picocolors: 1.1.1 1111 | source-map-js: 1.2.1 1112 | 1113 | rollup@4.40.0: 1114 | dependencies: 1115 | '@types/estree': 1.0.7 1116 | optionalDependencies: 1117 | '@rollup/rollup-android-arm-eabi': 4.40.0 1118 | '@rollup/rollup-android-arm64': 4.40.0 1119 | '@rollup/rollup-darwin-arm64': 4.40.0 1120 | '@rollup/rollup-darwin-x64': 4.40.0 1121 | '@rollup/rollup-freebsd-arm64': 4.40.0 1122 | '@rollup/rollup-freebsd-x64': 4.40.0 1123 | '@rollup/rollup-linux-arm-gnueabihf': 4.40.0 1124 | '@rollup/rollup-linux-arm-musleabihf': 4.40.0 1125 | '@rollup/rollup-linux-arm64-gnu': 4.40.0 1126 | '@rollup/rollup-linux-arm64-musl': 4.40.0 1127 | '@rollup/rollup-linux-loongarch64-gnu': 4.40.0 1128 | '@rollup/rollup-linux-powerpc64le-gnu': 4.40.0 1129 | '@rollup/rollup-linux-riscv64-gnu': 4.40.0 1130 | '@rollup/rollup-linux-riscv64-musl': 4.40.0 1131 | '@rollup/rollup-linux-s390x-gnu': 4.40.0 1132 | '@rollup/rollup-linux-x64-gnu': 4.40.0 1133 | '@rollup/rollup-linux-x64-musl': 4.40.0 1134 | '@rollup/rollup-win32-arm64-msvc': 4.40.0 1135 | '@rollup/rollup-win32-ia32-msvc': 4.40.0 1136 | '@rollup/rollup-win32-x64-msvc': 4.40.0 1137 | fsevents: 2.3.3 1138 | 1139 | siginfo@2.0.0: {} 1140 | 1141 | source-map-js@1.2.1: {} 1142 | 1143 | ssim.js@3.5.0: {} 1144 | 1145 | stackback@0.0.2: {} 1146 | 1147 | std-env@3.9.0: {} 1148 | 1149 | supports-color@7.2.0: 1150 | dependencies: 1151 | has-flag: 4.0.0 1152 | 1153 | tinybench@2.9.0: {} 1154 | 1155 | tinyexec@0.3.2: {} 1156 | 1157 | tinyglobby@0.2.13: 1158 | dependencies: 1159 | fdir: 6.4.4(picomatch@4.0.2) 1160 | picomatch: 4.0.2 1161 | 1162 | tinypool@1.0.2: {} 1163 | 1164 | tinyrainbow@2.0.0: {} 1165 | 1166 | tinyspy@3.0.2: {} 1167 | 1168 | ts-graphviz@2.1.6: 1169 | dependencies: 1170 | '@ts-graphviz/adapter': 2.0.6 1171 | '@ts-graphviz/ast': 2.0.7 1172 | '@ts-graphviz/common': 2.1.5 1173 | '@ts-graphviz/core': 2.0.7 1174 | 1175 | typescript@5.8.3: {} 1176 | 1177 | undici-types@6.21.0: {} 1178 | 1179 | vite-node@3.1.3(@types/node@22.15.18)(yaml@2.8.0): 1180 | dependencies: 1181 | cac: 6.7.14 1182 | debug: 4.4.0 1183 | es-module-lexer: 1.7.0 1184 | pathe: 2.0.3 1185 | vite: 6.3.3(@types/node@22.15.18)(yaml@2.8.0) 1186 | transitivePeerDependencies: 1187 | - '@types/node' 1188 | - jiti 1189 | - less 1190 | - lightningcss 1191 | - sass 1192 | - sass-embedded 1193 | - stylus 1194 | - sugarss 1195 | - supports-color 1196 | - terser 1197 | - tsx 1198 | - yaml 1199 | 1200 | vite@6.3.3(@types/node@22.15.18)(yaml@2.8.0): 1201 | dependencies: 1202 | esbuild: 0.25.3 1203 | fdir: 6.4.4(picomatch@4.0.2) 1204 | picomatch: 4.0.2 1205 | postcss: 8.5.3 1206 | rollup: 4.40.0 1207 | tinyglobby: 0.2.13 1208 | optionalDependencies: 1209 | '@types/node': 22.15.18 1210 | fsevents: 2.3.3 1211 | yaml: 2.8.0 1212 | 1213 | vitest@3.1.3(@types/node@22.15.18)(yaml@2.8.0): 1214 | dependencies: 1215 | '@vitest/expect': 3.1.3 1216 | '@vitest/mocker': 3.1.3(vite@6.3.3(@types/node@22.15.18)(yaml@2.8.0)) 1217 | '@vitest/pretty-format': 3.1.3 1218 | '@vitest/runner': 3.1.3 1219 | '@vitest/snapshot': 3.1.3 1220 | '@vitest/spy': 3.1.3 1221 | '@vitest/utils': 3.1.3 1222 | chai: 5.2.0 1223 | debug: 4.4.0 1224 | expect-type: 1.2.1 1225 | magic-string: 0.30.17 1226 | pathe: 2.0.3 1227 | std-env: 3.9.0 1228 | tinybench: 2.9.0 1229 | tinyexec: 0.3.2 1230 | tinyglobby: 0.2.13 1231 | tinypool: 1.0.2 1232 | tinyrainbow: 2.0.0 1233 | vite: 6.3.3(@types/node@22.15.18)(yaml@2.8.0) 1234 | vite-node: 3.1.3(@types/node@22.15.18)(yaml@2.8.0) 1235 | why-is-node-running: 2.3.0 1236 | optionalDependencies: 1237 | '@types/node': 22.15.18 1238 | transitivePeerDependencies: 1239 | - jiti 1240 | - less 1241 | - lightningcss 1242 | - msw 1243 | - sass 1244 | - sass-embedded 1245 | - stylus 1246 | - sugarss 1247 | - supports-color 1248 | - terser 1249 | - tsx 1250 | - yaml 1251 | 1252 | why-is-node-running@2.3.0: 1253 | dependencies: 1254 | siginfo: 2.0.0 1255 | stackback: 0.0.2 1256 | 1257 | yaml@2.8.0: {} 1258 | 1259 | zod-to-json-schema@3.24.5(zod@3.24.1): 1260 | dependencies: 1261 | zod: 3.24.1 1262 | 1263 | zod@3.24.1: {} 1264 | --------------------------------------------------------------------------------