Canvas: Generate graph state out of dodo-app config
Restructure code, create shared config lib.
Change-Id: I2cf06d35c486d4557484daf8618a2c215316fa7e
diff --git a/apps/canvas/back/.env b/apps/canvas/back/.env
index 6237047..a101621 100644
--- a/apps/canvas/back/.env
+++ b/apps/canvas/back/.env
@@ -1,3 +1,3 @@
-DATABASE_URL=file:${DODO_VOLUME_DATA}/dodo.db
-PUBLIC_ADDR=https://canvas.v1.dodo.cloud
-INTERNAL_API_ADDR=http://canvas-app.hgrz-dodo-app-gry.svc.cluster.local:8081
+DATABASE_URL=file:/home/gio/dodo.db
+PUBLIC_ADDR=https://canvas.p.v1.dodo.cloud
+INTERNAL_API_ADDR=http://canvas.hgrz-dodo-app-jjy.svc.cluster.local:8081
diff --git a/apps/canvas/back/package-lock.json b/apps/canvas/back/package-lock.json
index cd69cf4..03f0e80 100644
--- a/apps/canvas/back/package-lock.json
+++ b/apps/canvas/back/package-lock.json
@@ -12,6 +12,7 @@
"@loancrate/prisma-schema-parser": "^3.0.0",
"@prisma/client": "^6.6.0",
"axios": "^1.8.4",
+ "config": "file:../config",
"dotenv": "^16.5.0",
"dotenv-expand": "^12.0.2",
"express": "^4.21.1",
@@ -40,6 +41,21 @@
"typescript-eslint": "^8.11.0"
}
},
+ "../config": {
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "@xyflow/react": "^12.3.3",
+ "uuid": "^11.0.2",
+ "zod": "^3.24.4"
+ },
+ "devDependencies": {
+ "eslint": "^9.13.0",
+ "prettier": "3.5.3",
+ "typescript": "^5.8.3",
+ "typescript-eslint": "^8.11.0"
+ }
+ },
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
@@ -4279,6 +4295,10 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"devOptional": true
},
+ "node_modules/config": {
+ "resolved": "../config",
+ "link": true
+ },
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
diff --git a/apps/canvas/back/package.json b/apps/canvas/back/package.json
index 5f69e57..b32b801 100644
--- a/apps/canvas/back/package.json
+++ b/apps/canvas/back/package.json
@@ -3,7 +3,7 @@
"version": "1.0.0",
"description": "",
"main": "index.js",
- "type": "commonjs",
+ "type": "module",
"scripts": {
"build": "tsc",
"test": "jest",
@@ -18,6 +18,7 @@
"@loancrate/prisma-schema-parser": "^3.0.0",
"@prisma/client": "^6.6.0",
"axios": "^1.8.4",
+ "config": "file:../config",
"dotenv": "^16.5.0",
"dotenv-expand": "^12.0.2",
"express": "^4.21.1",
diff --git a/apps/canvas/back/src/app_manager.ts b/apps/canvas/back/src/app_manager.ts
index 803192c..faf64e2 100644
--- a/apps/canvas/back/src/app_manager.ts
+++ b/apps/canvas/back/src/app_manager.ts
@@ -69,7 +69,6 @@
if (response.status !== 200) {
throw new Error(`Failed to deploy application: ${response.statusText}`);
}
- console.log(response.data);
const result = DeployResponseSchema.safeParse(response.data);
if (!result.success) {
throw new Error(`Invalid deploy response format: ${result.error.message}`);
diff --git a/apps/canvas/back/src/index.ts b/apps/canvas/back/src/index.ts
index a2e8b31..0ba25b0 100644
--- a/apps/canvas/back/src/index.ts
+++ b/apps/canvas/back/src/index.ts
@@ -3,15 +3,16 @@
import fs from "node:fs";
import { env } from "node:process";
import axios from "axios";
-import { GithubClient } from "./github";
-import { AppManager } from "./app_manager";
+import { GithubClient } from "./github.js";
+import { AppManager } from "./app_manager.js";
import { z } from "zod";
-import { ProjectMonitor, WorkerSchema } from "./project_monitor";
+import { ProjectMonitor, WorkerSchema } from "./project_monitor.js";
import tmp from "tmp";
-import { NodeJSAnalyzer } from "./lib/nodejs";
+import { NodeJSAnalyzer } from "./lib/nodejs.js";
import shell from "shelljs";
-import { RealFileSystem } from "./lib/fs";
+import { RealFileSystem } from "./lib/fs.js";
import path from "node:path";
+import { Env, generateDodoConfig, ConfigSchema, AppNode, ConfigWithInput, configToGraph, Network } from "config";
async function generateKey(root: string): Promise<[string, string]> {
const privKeyPath = path.join(root, "key");
@@ -120,31 +121,44 @@
}
resp.status(200);
resp.header("content-type", "application/json");
+ let currentState: Record<string, unknown> | null = null;
if (state === "deploy") {
if (r.state == null) {
- resp.send({
+ currentState = {
nodes: [],
edges: [],
viewport: { x: 0, y: 0, zoom: 1 },
- });
+ };
} else {
- resp.send(JSON.parse(Buffer.from(r.state).toString("utf8")));
+ currentState = JSON.parse(Buffer.from(r.state).toString("utf8"));
}
} else {
if (r.draft == null) {
if (r.state == null) {
- resp.send({
+ currentState = {
nodes: [],
edges: [],
viewport: { x: 0, y: 0, zoom: 1 },
- });
+ };
} else {
- resp.send(JSON.parse(Buffer.from(r.state).toString("utf8")));
+ currentState = JSON.parse(Buffer.from(r.state).toString("utf8"));
}
} else {
- resp.send(JSON.parse(Buffer.from(r.draft).toString("utf8")));
+ currentState = JSON.parse(Buffer.from(r.draft).toString("utf8"));
}
}
+ const env = await getEnv(Number(req.params["projectId"]), resp.locals.userId, resp.locals.username);
+ if (currentState) {
+ const config = generateDodoConfig(
+ req.params["projectId"].toString(),
+ currentState.nodes as AppNode[],
+ env,
+ );
+ resp.send({
+ state: currentState,
+ config,
+ });
+ }
} catch (e) {
console.log(e);
resp.status(500);
@@ -257,7 +271,6 @@
deployKey: string,
publicAddr?: string,
): Promise<void> {
- console.log(publicAddr);
for (const repoUrl of diff.toDelete ?? []) {
try {
await github.removeDeployKey(repoUrl, deployKey);
@@ -289,11 +302,10 @@
const handleDeploy: express.Handler = async (req, resp) => {
try {
const projectId = Number(req.params["projectId"]);
- const state = JSON.stringify(req.body.state);
const p = await db.project.findUnique({
where: {
id: projectId,
- userId: resp.locals.userId,
+ // userId: resp.locals.userId, TODO(gio): validate
},
select: {
instanceId: true,
@@ -307,6 +319,21 @@
resp.status(404);
return;
}
+ const config = ConfigSchema.safeParse(req.body.config);
+ if (!config.success) {
+ resp.status(400);
+ resp.write(JSON.stringify({ error: "Invalid configuration", issues: config.error.format() }));
+ return;
+ }
+ const state = req.body.state
+ ? JSON.stringify(req.body.state)
+ : JSON.stringify(
+ configToGraph(
+ config.data,
+ getNetworks(resp.locals.username),
+ p.state ? JSON.parse(Buffer.from(p.state).toString("utf8")) : null,
+ ),
+ );
await db.project.update({
where: {
id: projectId,
@@ -325,14 +352,20 @@
});
}
let diff: RepoDiff | null = null;
- const config = req.body.config;
- config.input.key = {
- public: deployKeyPublic,
- private: deployKey,
+ const cfg: ConfigWithInput = {
+ ...config.data,
+ input: {
+ appId: projectId.toString(),
+ managerAddr: env.INTERNAL_API_ADDR!,
+ key: {
+ public: deployKeyPublic!,
+ private: deployKey!,
+ },
+ },
};
try {
if (p.instanceId == null) {
- const deployResponse = await appManager.deploy(config);
+ const deployResponse = await appManager.deploy(cfg);
await db.project.update({
where: {
id: projectId,
@@ -346,7 +379,7 @@
});
diff = { toAdd: extractGithubRepos(state) };
} else {
- const deployResponse = await appManager.update(p.instanceId, config);
+ const deployResponse = await appManager.update(p.instanceId, cfg);
diff = calculateRepoDiff(extractGithubRepos(p.state), extractGithubRepos(state));
await db.project.update({
where: {
@@ -413,6 +446,41 @@
}
};
+const handleConfigGet: express.Handler = async (req, resp) => {
+ try {
+ const projectId = Number(req.params["projectId"]);
+ const project = await db.project.findUnique({
+ where: {
+ id: projectId,
+ },
+ select: {
+ state: true,
+ },
+ });
+
+ if (!project || !project.state) {
+ resp.status(404).send({ error: "No deployed configuration found." });
+ return;
+ }
+
+ const state = JSON.parse(Buffer.from(project.state).toString("utf8"));
+ const env = await getEnv(projectId, resp.locals.userId, resp.locals.username);
+ const config = generateDodoConfig(projectId.toString(), state.nodes, env);
+
+ if (!config) {
+ resp.status(500).send({ error: "Failed to generate configuration." });
+ return;
+ }
+ resp.status(200).json(config);
+ } catch (e) {
+ console.log(e);
+ resp.status(500).send({ error: "Internal server error" });
+ } finally {
+ console.log("config get done");
+ resp.end();
+ }
+};
+
const handleRemoveDeployment: express.Handler = async (req, resp) => {
try {
const projectId = Number(req.params["projectId"]);
@@ -529,80 +597,84 @@
}
};
+const getNetworks = (username?: string | undefined): Network[] => {
+ return [
+ {
+ name: "Trial",
+ domain: "trial.dodoapp.xyz",
+ hasAuth: false,
+ },
+ // TODO(gio): Remove
+ ].concat(
+ username === "gio" || 1 == 1
+ ? [
+ {
+ name: "Public",
+ domain: "v1.dodo.cloud",
+ hasAuth: true,
+ },
+ {
+ name: "Private",
+ domain: "p.v1.dodo.cloud",
+ hasAuth: true,
+ },
+ ]
+ : [],
+ );
+};
+
+const getEnv = async (projectId: number, userId: string, username: string): Promise<Env> => {
+ const project = await db.project.findUnique({
+ where: {
+ id: projectId,
+ userId,
+ },
+ select: {
+ deployKeyPublic: true,
+ githubToken: true,
+ access: true,
+ instanceId: true,
+ },
+ });
+ if (!project) {
+ throw new Error("Project not found");
+ }
+ const monitor = projectMonitors.get(projectId);
+ const serviceNames = monitor ? monitor.getAllServiceNames() : [];
+ const services = serviceNames.map((name: string) => ({
+ name,
+ workers: [...(monitor ? monitor.getWorkerStatusesForService(name) : new Map()).entries()].map(
+ ([id, status]) => ({
+ ...status,
+ id,
+ }),
+ ),
+ }));
+ return {
+ managerAddr: env.INTERNAL_API_ADDR,
+ deployKeyPublic: project.deployKeyPublic == null ? undefined : project.deployKeyPublic,
+ instanceId: project.instanceId == null ? undefined : project.instanceId,
+ access: JSON.parse(project.access ?? "[]"),
+ integrations: {
+ github: !!project.githubToken,
+ },
+ networks: getNetworks(username),
+ services,
+ user: {
+ id: userId,
+ username: username,
+ },
+ };
+};
+
const handleEnv: express.Handler = async (req, resp) => {
const projectId = Number(req.params["projectId"]);
try {
- const project = await db.project.findUnique({
- where: {
- id: projectId,
- userId: resp.locals.userId,
- },
- select: {
- deployKeyPublic: true,
- githubToken: true,
- access: true,
- instanceId: true,
- },
- });
- if (!project) {
- resp.status(404);
- resp.write(JSON.stringify({ error: "Project not found" }));
- return;
- }
- const monitor = projectMonitors.get(projectId);
- const serviceNames = monitor ? monitor.getAllServiceNames() : [];
- const services = serviceNames.map((name) => ({
- name,
- workers: [...(monitor ? monitor.getWorkerStatusesForService(name) : new Map()).entries()].map(
- ([id, status]) => ({
- ...status,
- id,
- }),
- ),
- }));
-
+ const env = await getEnv(projectId, resp.locals.userId, resp.locals.username);
resp.status(200);
- resp.write(
- JSON.stringify({
- managerAddr: env.INTERNAL_API_ADDR,
- deployKeyPublic: project.deployKeyPublic == null ? undefined : project.deployKeyPublic,
- instanceId: project.instanceId == null ? undefined : project.instanceId,
- access: JSON.parse(project.access ?? "[]"),
- integrations: {
- github: !!project.githubToken,
- },
- networks: [
- {
- name: "Trial",
- domain: "trial.dodoapp.xyz",
- hasAuth: false,
- },
- // TODO(gio): Remove
- ].concat(
- resp.locals.username !== "gio"
- ? []
- : [
- {
- name: "Public",
- domain: "v1.dodo.cloud",
- hasAuth: true,
- },
- {
- name: "Private",
- domain: "p.v1.dodo.cloud",
- hasAuth: true,
- },
- ],
- ),
- services,
- user: {
- id: resp.locals.userId,
- username: resp.locals.username,
- },
- }),
- );
+ resp.write(JSON.stringify(env));
} catch (error) {
- console.error("Error checking integrations:", error);
+ console.error("Error getting env:", error);
resp.status(500);
resp.write(JSON.stringify({ error: "Internal server error" }));
} finally {
@@ -692,12 +764,17 @@
return true;
}
const results = await Promise.all(
- projectWorkers.map(async (workerAddress) => {
- const resp = await axios.post(`${workerAddress}/update`);
- return resp.status === 200;
+ projectWorkers.map(async (workerAddress: string) => {
+ try {
+ const { data } = await axios.get(`http://${workerAddress}/reload`);
+ return data.every((s: { status: string }) => s.status === "ok");
+ } catch (error) {
+ console.error(`Failed to reload worker ${workerAddress}:`, error);
+ return false;
+ }
}),
);
- return results.reduce((acc, curr) => acc && curr, true);
+ return results.reduce((acc: boolean, curr: boolean) => acc && curr, true);
}
const handleReload: express.Handler = async (req, resp) => {
@@ -886,9 +963,30 @@
}
};
+const handleValidateConfig: express.Handler = async (req, resp) => {
+ try {
+ const validationResult = ConfigSchema.safeParse(req.body);
+ if (!validationResult.success) {
+ resp.status(400);
+ resp.header("Content-Type", "application/json");
+ resp.write(JSON.stringify({ success: false, errors: validationResult.error.flatten() }));
+ } else {
+ resp.status(200);
+ resp.header("Content-Type", "application/json");
+ resp.write(JSON.stringify({ success: true }));
+ }
+ } catch (e) {
+ console.log(e);
+ resp.status(500);
+ } finally {
+ resp.end();
+ }
+};
+
async function start() {
await db.$connect();
const app = express();
+ app.set("json spaces", 2);
app.use(express.json()); // Global JSON parsing
// Public webhook route - no auth needed
@@ -903,6 +1001,7 @@
projectRouter.get("/:projectId/saved/draft", handleSavedGet("draft"));
projectRouter.post("/:projectId/deploy", handleDeploy);
projectRouter.get("/:projectId/status", handleStatus);
+ projectRouter.get("/:projectId/config", handleConfigGet);
projectRouter.delete("/:projectId", handleProjectDelete);
projectRouter.get("/:projectId/repos/github", handleGithubRepos);
projectRouter.post("/:projectId/github-token", handleUpdateGithubToken);
@@ -921,6 +1020,9 @@
const internalApi = express();
internalApi.use(express.json());
internalApi.post("/api/project/:projectId/workers", handleRegisterWorker);
+ internalApi.get("/api/project/:projectId/config", handleConfigGet);
+ internalApi.post("/api/project/:projectId/deploy", handleDeploy);
+ internalApi.post("/api/validate-config", handleValidateConfig);
app.listen(env.DODO_PORT_WEB, () => {
console.log("Web server started on port", env.DODO_PORT_WEB);
diff --git a/apps/canvas/back/src/lib/analyze.ts b/apps/canvas/back/src/lib/analyze.ts
index e1fa699..28eecbf 100644
--- a/apps/canvas/back/src/lib/analyze.ts
+++ b/apps/canvas/back/src/lib/analyze.ts
@@ -1,4 +1,4 @@
-import { FileSystem } from "./fs";
+import { FileSystem } from "./fs.js";
export interface ServiceAnalyzer {
detect: (fs: FileSystem, root: string) => boolean;
diff --git a/apps/canvas/back/src/lib/nodejs.test.ts b/apps/canvas/back/src/lib/nodejs.test.ts
index 7d406b1..5de8423 100644
--- a/apps/canvas/back/src/lib/nodejs.test.ts
+++ b/apps/canvas/back/src/lib/nodejs.test.ts
@@ -1,8 +1,8 @@
-import { NodeJSAnalyzer } from "./nodejs";
-import { FileSystem, RealFileSystem } from "./fs";
+import { NodeJSAnalyzer } from "./nodejs.js";
+import { FileSystem, RealFileSystem } from "./fs.js";
import { Volume, IFs, createFsFromVolume } from "memfs";
import { test, expect } from "@jest/globals";
-import { expandValue } from "./env";
+import { expandValue } from "./env.js";
import shell from "shelljs";
class InMemoryFileSystem implements FileSystem {
diff --git a/apps/canvas/back/src/lib/nodejs.ts b/apps/canvas/back/src/lib/nodejs.ts
index 07e6c1f..3369b1d 100644
--- a/apps/canvas/back/src/lib/nodejs.ts
+++ b/apps/canvas/back/src/lib/nodejs.ts
@@ -1,10 +1,10 @@
import path from "path";
-import { FileSystem } from "./fs";
-import { ServiceAnalyzer, ConfigVar, ConfigVarCategory, ConfigVarSemanticType } from "./analyze";
+import { FileSystem } from "./fs.js";
+import { ServiceAnalyzer, ConfigVar, ConfigVarCategory, ConfigVarSemanticType } from "./analyze.js";
import { parse as parseDotenv } from "dotenv";
import { parsePrismaSchema } from "@loancrate/prisma-schema-parser";
-import { augmentConfigVar } from "./semantics";
-import { expandValue } from "./env";
+import { augmentConfigVar } from "./semantics.js";
+import { expandValue } from "./env.js";
import { z } from "zod";
const packageJsonFileName = "package.json";
diff --git a/apps/canvas/back/src/lib/semantics.ts b/apps/canvas/back/src/lib/semantics.ts
index 2cbd1ab..f365f83 100644
--- a/apps/canvas/back/src/lib/semantics.ts
+++ b/apps/canvas/back/src/lib/semantics.ts
@@ -1,4 +1,4 @@
-import { ConfigVar, ConfigVarSemanticType } from "./analyze";
+import { ConfigVar, ConfigVarSemanticType } from "./analyze.js";
export function augmentConfigVar(cv: ConfigVar) {
if (cv.semanticType != null) {
diff --git a/apps/canvas/back/tsconfig.json b/apps/canvas/back/tsconfig.json
index c0ce9b7..b92e830 100644
--- a/apps/canvas/back/tsconfig.json
+++ b/apps/canvas/back/tsconfig.json
@@ -13,7 +13,7 @@
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
- "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
+ "target": "es2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
@@ -26,7 +26,10 @@
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
- "module": "commonjs" /* Specify what module code is generated. */,
+ "module": "node16",
+ "moduleResolution": "node16",
+ "allowImportingTsExtensions": false,
+ "noEmit": false,
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
@@ -49,7 +52,7 @@
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
- // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
+ "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
diff --git a/apps/canvas/config/eslint.config.mjs b/apps/canvas/config/eslint.config.mjs
new file mode 100644
index 0000000..20ef473
--- /dev/null
+++ b/apps/canvas/config/eslint.config.mjs
@@ -0,0 +1,23 @@
+import js from "@eslint/js";
+import tseslint from "typescript-eslint";
+
+export default tseslint.config(
+ { ignores: ["dist"] },
+ {
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
+ files: ["**/*.ts"],
+ languageOptions: {
+ ecmaVersion: 2020,
+ },
+ rules: {
+ "@typescript-eslint/no-unused-vars": [
+ "error",
+ {
+ argsIgnorePattern: "^_$",
+ varsIgnorePattern: "^_$",
+ caughtErrorsIgnorePattern: "^_$",
+ },
+ ],
+ },
+ },
+);
diff --git a/apps/canvas/config/package-lock.json b/apps/canvas/config/package-lock.json
new file mode 100644
index 0000000..ba8253b
--- /dev/null
+++ b/apps/canvas/config/package-lock.json
@@ -0,0 +1,1952 @@
+{
+ "name": "config",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "config",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "@xyflow/react": "^12.3.3",
+ "uuid": "^11.0.2",
+ "zod": "^3.24.4"
+ },
+ "devDependencies": {
+ "eslint": "^9.13.0",
+ "prettier": "3.5.3",
+ "typescript": "^5.8.3",
+ "typescript-eslint": "^8.11.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
+ "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+ "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.20.1",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.1.tgz",
+ "integrity": "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.6",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.3.tgz",
+ "integrity": "sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz",
+ "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
+ "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.29.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz",
+ "integrity": "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
+ "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.2.tgz",
+ "integrity": "sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.15.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.0.tgz",
+ "integrity": "sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.6",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
+ "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
+ "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-drag": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+ "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-selection": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
+ "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-transition": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
+ "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-zoom": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+ "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-interpolate": "*",
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.34.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.0.tgz",
+ "integrity": "sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.10.0",
+ "@typescript-eslint/scope-manager": "8.34.0",
+ "@typescript-eslint/type-utils": "8.34.0",
+ "@typescript-eslint/utils": "8.34.0",
+ "@typescript-eslint/visitor-keys": "8.34.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^7.0.0",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.34.0",
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.34.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.0.tgz",
+ "integrity": "sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.34.0",
+ "@typescript-eslint/types": "8.34.0",
+ "@typescript-eslint/typescript-estree": "8.34.0",
+ "@typescript-eslint/visitor-keys": "8.34.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.34.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.0.tgz",
+ "integrity": "sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.34.0",
+ "@typescript-eslint/types": "^8.34.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.34.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.0.tgz",
+ "integrity": "sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.34.0",
+ "@typescript-eslint/visitor-keys": "8.34.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.34.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.0.tgz",
+ "integrity": "sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.34.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.0.tgz",
+ "integrity": "sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "8.34.0",
+ "@typescript-eslint/utils": "8.34.0",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.34.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.0.tgz",
+ "integrity": "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.34.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.0.tgz",
+ "integrity": "sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.34.0",
+ "@typescript-eslint/tsconfig-utils": "8.34.0",
+ "@typescript-eslint/types": "8.34.0",
+ "@typescript-eslint/visitor-keys": "8.34.0",
+ "debug": "^4.3.4",
+ "fast-glob": "^3.3.2",
+ "is-glob": "^4.0.3",
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.34.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.0.tgz",
+ "integrity": "sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.7.0",
+ "@typescript-eslint/scope-manager": "8.34.0",
+ "@typescript-eslint/types": "8.34.0",
+ "@typescript-eslint/typescript-estree": "8.34.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.34.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.0.tgz",
+ "integrity": "sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.34.0",
+ "eslint-visitor-keys": "^4.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@xyflow/react": {
+ "version": "12.7.0",
+ "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.7.0.tgz",
+ "integrity": "sha512-U6VMEbYjiCg1byHrR7S+b5ZdHTjgCFX4KpBc634G/WtEBUvBLoMQdlCD6uJHqodnOAxpt3+G2wiDeTmXAFJzgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@xyflow/system": "0.0.62",
+ "classcat": "^5.0.3",
+ "zustand": "^4.4.0"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@xyflow/system": {
+ "version": "0.0.62",
+ "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.62.tgz",
+ "integrity": "sha512-Z2ufbnvuYxIOCGyzE/8eX8TAEM8Lpzc/JafjD1Tzy6ZJs/E7KGVU17Q1F5WDHVW+dbztJAdyXMG0ejR9bwSUAA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-drag": "^3.0.7",
+ "@types/d3-interpolate": "^3.0.4",
+ "@types/d3-selection": "^3.0.10",
+ "@types/d3-transition": "^3.0.8",
+ "@types/d3-zoom": "^3.0.8",
+ "d3-drag": "^3.0.0",
+ "d3-interpolate": "^3.0.1",
+ "d3-selection": "^3.0.0",
+ "d3-zoom": "^3.0.0"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/classcat": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
+ "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
+ "license": "MIT"
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.29.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.29.0.tgz",
+ "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.20.1",
+ "@eslint/config-helpers": "^0.2.1",
+ "@eslint/core": "^0.14.0",
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "9.29.0",
+ "@eslint/plugin-kit": "^0.3.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "@types/json-schema": "^7.0.15",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fastq": {
+ "version": "1.19.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
+ "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
+ "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/react": {
+ "version": "19.1.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
+ "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.1.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
+ "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "scheduler": "^0.26.0"
+ },
+ "peerDependencies": {
+ "react": "^19.1.0"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.26.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
+ "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
+ "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.8.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
+ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.34.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.34.0.tgz",
+ "integrity": "sha512-MRpfN7uYjTrTGigFCt8sRyNqJFhjN0WwZecldaqhWm+wy0gaRt8Edb/3cuUy0zdq2opJWT6iXINKAtewnDOltQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.34.0",
+ "@typescript-eslint/parser": "8.34.0",
+ "@typescript-eslint/utils": "8.34.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
+ "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
+ "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/esm/bin/uuid"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "3.25.64",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.64.tgz",
+ "integrity": "sha512-hbP9FpSZf7pkS7hRVUrOjhwKJNyampPgtXKc3AN6DsWtoHsg2Sb4SQaS4Tcay380zSwd2VPo9G9180emBACp5g==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zustand": {
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+ "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.2.2"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0.6",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/apps/canvas/config/package.json b/apps/canvas/config/package.json
new file mode 100644
index 0000000..c5b04e6
--- /dev/null
+++ b/apps/canvas/config/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "config",
+ "version": "1.0.0",
+ "description": "",
+ "license": "ISC",
+ "author": "",
+ "type": "module",
+ "main": "dist/index.js",
+ "scripts": {
+ "build": "tsc",
+ "format": "prettier --write src/**/*.{js,ts,jsx,tsx} --list-different",
+ "format-check": "prettier --check src/**/*.{js,ts,jsx,tsx}",
+ "lint": "eslint ."
+ },
+ "dependencies": {
+ "zod": "^3.24.4",
+ "@xyflow/react": "^12.3.3",
+ "uuid": "^11.0.2"
+ },
+ "devDependencies": {
+ "eslint": "^9.13.0",
+ "prettier": "3.5.3",
+ "typescript": "^5.8.3",
+ "typescript-eslint": "^8.11.0"
+ },
+ "prettier": {
+ "printWidth": 120,
+ "tabWidth": 4,
+ "useTabs": true,
+ "singleQuote": false,
+ "trailingComma": "all",
+ "arrowParens": "always"
+ }
+}
diff --git a/apps/canvas/config/src/config.ts b/apps/canvas/config/src/config.ts
new file mode 100644
index 0000000..53fd64c
--- /dev/null
+++ b/apps/canvas/config/src/config.ts
@@ -0,0 +1,598 @@
+import {
+ AppNode,
+ BoundEnvVar,
+ Env,
+ GatewayHttpsNode,
+ GatewayTCPNode,
+ MongoDBNode,
+ Network,
+ NetworkNode,
+ Port,
+ PostgreSQLNode,
+ ServiceNode,
+ VolumeNode,
+} from "./graph.js";
+import { Edge } from "@xyflow/react";
+import { v4 as uuidv4 } from "uuid";
+import { ConfigWithInput, Ingress, Service, Volume, PostgreSQL, MongoDB, Config, PortDomain } from "./types.js";
+
+export function generateDodoConfig(appId: string | undefined, nodes: AppNode[], env: Env): ConfigWithInput | null {
+ try {
+ if (appId == null || env.managerAddr == null) {
+ return null;
+ }
+ const networkMap = new Map(env.networks.map((n) => [n.domain, n.name]));
+ const ingressNodes = nodes
+ .filter((n) => n.type === "gateway-https")
+ .filter((n) => n.data.https !== undefined && !n.data.readonly);
+ const tcpNodes = nodes
+ .filter((n) => n.type === "gateway-tcp")
+ .filter((n) => n.data.exposed !== undefined && !n.data.readonly);
+ const findExpose = (n: AppNode): PortDomain[] => {
+ return n.data.ports
+ .map((p) => [n.id, p.id, p.name])
+ .flatMap((sp) => {
+ return tcpNodes.flatMap((i) =>
+ (i.data.exposed || [])
+ .filter((t) => t.serviceId === sp[0] && t.portId === sp[1])
+ .map(() => ({
+ nodeId: i.id,
+ network: networkMap.get(i.data.network!)!,
+ subdomain: i.data.subdomain!,
+ port: { name: sp[2] },
+ })),
+ );
+ });
+ };
+ return {
+ input: {
+ appId: appId,
+ managerAddr: env.managerAddr,
+ },
+ service: nodes
+ .filter((n) => n.type === "app")
+ .map((n): Service => {
+ return {
+ nodeId: n.id,
+ type: n.data.type,
+ name: n.data.label,
+ source: {
+ repository: nodes
+ .filter((i) => i.type === "github")
+ .find((i) => i.id === n.data.repository?.repoNodeId)!.data.repository!.sshURL,
+ branch:
+ n.data.repository != undefined && "branch" in n.data.repository
+ ? n.data.repository.branch
+ : "main",
+ rootDir:
+ n.data.repository != undefined && "rootDir" in n.data.repository
+ ? n.data.repository.rootDir
+ : "/",
+ },
+ ports: (n.data.ports || [])
+ .filter((p) => !n.data.dev?.enabled || (p.value != 22 && p.value != 9090))
+ .map((p) => ({
+ name: p.name.toLowerCase(),
+ value: p.value,
+ protocol: "TCP", // TODO(gio)
+ })),
+ env: (n.data.envVars || [])
+ .filter((e) => "name" in e)
+ .map((e) => ({
+ name: e.name,
+ alias: "alias" in e ? e.alias : undefined,
+ })),
+ ingress: ingressNodes
+ .filter((i) => i.data.https!.serviceId === n.id)
+ .map(
+ (i): Ingress => ({
+ nodeId: i.id,
+ network: networkMap.get(i.data.network!)!,
+ subdomain: i.data.subdomain!,
+ port: {
+ name: n.data.ports.find((p) => p.id === i.data.https!.portId)!.name,
+ },
+ auth:
+ i.data.auth?.enabled || false
+ ? {
+ enabled: true,
+ groups: i.data.auth!.groups,
+ noAuthPathPatterns: i.data.auth!.noAuthPathPatterns,
+ }
+ : {
+ enabled: false,
+ },
+ }),
+ ),
+ expose: findExpose(n),
+ preBuildCommands: n.data.preBuildCommands
+ ? n.data.preBuildCommands.split("\n").map((cmd) => ({ bin: cmd }))
+ : [],
+ dev: {
+ enabled: n.data.dev ? n.data.dev.enabled : false,
+ username: n.data.dev && n.data.dev.enabled ? env.user.username : undefined,
+ codeServer:
+ n.data.dev?.enabled && n.data.dev.expose != null
+ ? {
+ network: networkMap.get(n.data.dev.expose.network)!,
+ subdomain: n.data.dev.expose.subdomain,
+ }
+ : undefined,
+ ssh:
+ n.data.dev?.enabled && n.data.dev.expose != null
+ ? {
+ network: networkMap.get(n.data.dev.expose.network)!,
+ subdomain: n.data.dev.expose.subdomain,
+ }
+ : undefined,
+ },
+ };
+ }),
+ volume: nodes
+ .filter((n) => n.type === "volume")
+ .map(
+ (n): Volume => ({
+ nodeId: n.id,
+ name: n.data.label,
+ accessMode: n.data.type,
+ size: n.data.size,
+ }),
+ ),
+ postgresql: nodes
+ .filter((n) => n.type === "postgresql")
+ .map(
+ (n): PostgreSQL => ({
+ nodeId: n.id,
+ name: n.data.label,
+ size: "1Gi", // TODO(gio)
+ expose: findExpose(n).map((e) => ({ network: e.network, subdomain: e.subdomain })),
+ }),
+ ),
+ mongodb: nodes
+ .filter((n) => n.type === "mongodb")
+ .map(
+ (n): MongoDB => ({
+ nodeId: n.id,
+ name: n.data.label,
+ size: "1Gi", // TODO(gio)
+ expose: findExpose(n).map((e) => ({ network: e.network, subdomain: e.subdomain })),
+ }),
+ ),
+ };
+ } catch (e) {
+ console.log(e);
+ return { input: { appId: "qweqwe", managerAddr: "" } };
+ }
+}
+
+export type Graph = {
+ nodes: AppNode[];
+ edges: Edge[];
+};
+
+export function configToGraph(config: Config, networks: Network[], current?: Graph): Graph {
+ if (current == null) {
+ current = { nodes: [], edges: [] };
+ }
+ const ret: Graph = {
+ nodes: [],
+ edges: [],
+ };
+ if (networks.length === 0) {
+ return ret;
+ }
+ const networkNodes = networks.map((n): NetworkNode => {
+ let existing: NetworkNode | undefined = undefined;
+ existing = current.nodes
+ .filter((i): i is NetworkNode => i.type === "network")
+ .find((i) => i.data.domain === n.domain);
+ return {
+ id: n.domain,
+ type: "network",
+ data: {
+ label: n.name,
+ domain: n.domain,
+ envVars: [],
+ ports: [],
+ },
+ position: existing != null ? existing.position : { x: 0, y: 0 },
+ };
+ });
+ const services = config.service?.map((s): ServiceNode => {
+ let existing: ServiceNode | null = null;
+ if (s.nodeId !== undefined) {
+ existing = current.nodes.find((n) => n.id === s.nodeId) as ServiceNode;
+ }
+ return {
+ id: existing != null ? existing.id : uuidv4(),
+ type: "app",
+ data: {
+ label: s.name,
+ type: s.type,
+ env: [],
+ ports: (s.ports || []).map(
+ (p): Port => ({
+ id: uuidv4(),
+ name: p.name,
+ value: p.value,
+ }),
+ ),
+ envVars: (s.env || []).map((e): BoundEnvVar => {
+ if (e.alias != null) {
+ return {
+ id: uuidv4(),
+ name: e.name,
+ source: null,
+ alias: e.alias,
+ isEditting: false,
+ };
+ } else {
+ return {
+ id: uuidv4(),
+ name: e.name,
+ source: null,
+ isEditting: false,
+ };
+ }
+ }),
+ volume: s.volume || [],
+ preBuildCommands: s.preBuildCommands?.map((p) => p.bin).join("\n") || "",
+ // TODO(gio): dev
+ isChoosingPortToConnect: false,
+ },
+ // TODO(gio): generate position
+ position:
+ existing != null
+ ? existing.position
+ : {
+ x: 0,
+ y: 0,
+ },
+ };
+ });
+ const serviceGateways = config.service?.flatMap((s, index): GatewayHttpsNode[] => {
+ return (s.ingress || []).map((i): GatewayHttpsNode => {
+ let existing: GatewayHttpsNode | null = null;
+ if (i.nodeId !== undefined) {
+ existing = current.nodes.find((n) => n.id === i.nodeId) as GatewayHttpsNode;
+ }
+ console.log("!!!", i.network, networks);
+ return {
+ id: existing != null ? existing.id : uuidv4(),
+ type: "gateway-https",
+ data: {
+ label: i.subdomain,
+ envVars: [],
+ ports: [],
+ network: networks.find((n) => n.name.toLowerCase() === i.network.toLowerCase())!.domain,
+ subdomain: i.subdomain,
+ https: {
+ serviceId: services![index]!.id,
+ portId: services![index]!.data.ports.find((p) => {
+ const port = i.port;
+ if ("name" in port) {
+ return p.name === port.name;
+ } else {
+ return `${p.value}` === port.value;
+ }
+ })!.id,
+ },
+ auth: i.auth.enabled
+ ? {
+ enabled: true,
+ groups: i.auth.groups || [],
+ noAuthPathPatterns: i.auth.noAuthPathPatterns || [],
+ }
+ : {
+ enabled: false,
+ groups: [],
+ noAuthPathPatterns: [],
+ },
+ },
+ position: {
+ x: 0,
+ y: 0,
+ },
+ };
+ });
+ });
+ const exposures = new Map<string, GatewayTCPNode>();
+ config.service
+ ?.flatMap((s, index): GatewayTCPNode[] => {
+ return (s.expose || []).map((e): GatewayTCPNode => {
+ let existing: GatewayTCPNode | null = null;
+ if (e.nodeId !== undefined) {
+ existing = current.nodes.find((n) => n.id === e.nodeId) as GatewayTCPNode;
+ }
+ return {
+ id: existing != null ? existing.id : uuidv4(),
+ type: "gateway-tcp",
+ data: {
+ label: e.subdomain,
+ envVars: [],
+ ports: [],
+ network: networks.find((n) => n.name.toLowerCase() === e.network.toLowerCase())!.domain,
+ subdomain: e.subdomain,
+ exposed: [
+ {
+ serviceId: services![index]!.id,
+ portId: services![index]!.data.ports.find((p) => {
+ const port = e.port;
+ if ("name" in port) {
+ return p.name === port.name;
+ } else {
+ return p.value === port.value;
+ }
+ })!.id,
+ },
+ ],
+ },
+ position: existing != null ? existing.position : { x: 0, y: 0 },
+ };
+ });
+ })
+ .forEach((n) => {
+ const key = `${n.data.network}-${n.data.subdomain}`;
+ if (!exposures.has(key)) {
+ exposures.set(key, n);
+ } else {
+ exposures.get(key)!.data.exposed.push(...n.data.exposed);
+ }
+ });
+ const volumes = config.volume?.map((v): VolumeNode => {
+ let existing: VolumeNode | null = null;
+ if (v.nodeId !== undefined) {
+ existing = current.nodes.find((n) => n.id === v.nodeId) as VolumeNode;
+ }
+ return {
+ id: existing != null ? existing.id : uuidv4(),
+ type: "volume",
+ data: {
+ label: v.name,
+ type: v.accessMode,
+ size: v.size,
+ attachedTo: [],
+ envVars: [],
+ ports: [],
+ },
+ position:
+ existing != null
+ ? existing.position
+ : {
+ x: 0,
+ y: 0,
+ },
+ };
+ });
+ const postgresql = config.postgresql?.map((p): PostgreSQLNode => {
+ let existing: PostgreSQLNode | null = null;
+ if (p.nodeId !== undefined) {
+ existing = current.nodes.find((n) => n.id === p.nodeId) as PostgreSQLNode;
+ }
+ return {
+ id: existing != null ? existing.id : uuidv4(),
+ type: "postgresql",
+ data: {
+ label: p.name,
+ volumeId: "", // TODO(gio): volume
+ envVars: [],
+ ports: [
+ {
+ id: "connection",
+ name: "connection",
+ value: 5432,
+ },
+ ],
+ },
+ position:
+ existing != null
+ ? existing.position
+ : {
+ x: 0,
+ y: 0,
+ },
+ };
+ });
+ config.postgresql
+ ?.flatMap((p, index): GatewayTCPNode[] => {
+ return (p.expose || []).map((e): GatewayTCPNode => {
+ let existing: GatewayTCPNode | null = null;
+ if (e.nodeId !== undefined) {
+ existing = current.nodes.find((n) => n.id === e.nodeId) as GatewayTCPNode;
+ }
+ return {
+ id: existing != null ? existing.id : uuidv4(),
+ type: "gateway-tcp",
+ data: {
+ label: e.subdomain,
+ envVars: [],
+ ports: [],
+ network: networks.find((n) => n.name.toLowerCase() === e.network.toLowerCase())!.domain,
+ subdomain: e.subdomain,
+ exposed: [
+ {
+ serviceId: postgresql![index]!.id,
+ portId: "connection",
+ },
+ ],
+ },
+ position: existing != null ? existing.position : { x: 0, y: 0 },
+ };
+ });
+ })
+ .forEach((n) => {
+ const key = `${n.data.network}-${n.data.subdomain}`;
+ if (!exposures.has(key)) {
+ exposures.set(key, n);
+ } else {
+ exposures.get(key)!.data.exposed.push(...n.data.exposed);
+ }
+ });
+ const mongodb = config.mongodb?.map((m): MongoDBNode => {
+ let existing: MongoDBNode | null = null;
+ if (m.nodeId !== undefined) {
+ existing = current.nodes.find((n) => n.id === m.nodeId) as MongoDBNode;
+ }
+ return {
+ id: existing != null ? existing.id : uuidv4(),
+ type: "mongodb",
+ data: {
+ label: m.name,
+ volumeId: "", // TODO(gio): volume
+ envVars: [],
+ ports: [
+ {
+ id: "connection",
+ name: "connection",
+ value: 27017,
+ },
+ ],
+ },
+ position:
+ existing != null
+ ? existing.position
+ : {
+ x: 0,
+ y: 0,
+ },
+ };
+ });
+ config.mongodb
+ ?.flatMap((p, index): GatewayTCPNode[] => {
+ return (p.expose || []).map((e): GatewayTCPNode => {
+ let existing: GatewayTCPNode | null = null;
+ if (e.nodeId !== undefined) {
+ existing = current.nodes.find((n) => n.id === e.nodeId) as GatewayTCPNode;
+ }
+ return {
+ id: existing != null ? existing.id : uuidv4(),
+ type: "gateway-tcp",
+ data: {
+ label: e.subdomain,
+ envVars: [],
+ ports: [],
+ network: networks.find((n) => n.name.toLowerCase() === e.network.toLowerCase())!.domain,
+ subdomain: e.subdomain,
+ exposed: [
+ {
+ serviceId: mongodb![index]!.id,
+ portId: "connection",
+ },
+ ],
+ },
+ position: existing != null ? existing.position : { x: 0, y: 0 },
+ };
+ });
+ })
+ .forEach((n) => {
+ const key = `${n.data.network}-${n.data.subdomain}`;
+ if (!exposures.has(key)) {
+ exposures.set(key, n);
+ } else {
+ exposures.get(key)!.data.exposed.push(...n.data.exposed);
+ }
+ });
+ ret.nodes = [
+ ...networkNodes,
+ ...ret.nodes,
+ ...(services || []),
+ ...(serviceGateways || []),
+ ...(volumes || []),
+ ...(postgresql || []),
+ ...(mongodb || []),
+ ...(exposures.values() || []),
+ ];
+ services?.forEach((s) => {
+ s.data.envVars.forEach((e) => {
+ if (!("name" in e)) {
+ return;
+ }
+ if (!e.name.startsWith("DODO_")) {
+ return;
+ }
+ let r: {
+ type: string;
+ name: string;
+ } | null = null;
+ if (e.name.startsWith("DODO_PORT_")) {
+ return;
+ } else if (e.name.startsWith("DODO_POSTGRESQL_")) {
+ r = {
+ type: "postgresql",
+ name: e.name.replace("DODO_POSTGRESQL_", "").replace("_URL", "").toLowerCase(),
+ };
+ } else if (e.name.startsWith("DODO_MONGODB_")) {
+ r = {
+ type: "mongodb",
+ name: e.name.replace("DODO_MONGODB_", "").replace("_URL", "").toLowerCase(),
+ };
+ } else if (e.name.startsWith("DODO_VOLUME_")) {
+ r = {
+ type: "volume",
+ name: e.name.replace("DODO_VOLUME_", "").toLowerCase(),
+ };
+ }
+ if (r != null) {
+ e.source = ret.nodes.find((n) => n.type === r.type && n.data.label.toLowerCase() === r.name)!.id;
+ }
+ });
+ });
+ const envVarEdges = [...(services || [])].flatMap((n): Edge[] => {
+ return n.data.envVars.flatMap((e): Edge[] => {
+ if (e.source == null) {
+ return [];
+ }
+ const sn = ret.nodes.find((n) => n.id === e.source!)!;
+ const sourceHandle = sn.type === "app" ? "ports" : sn.type === "volume" ? "volume" : "env_var";
+ return [
+ {
+ id: uuidv4(),
+ source: e.source!,
+ sourceHandle: sourceHandle,
+ target: n.id,
+ targetHandle: "env_var",
+ },
+ ];
+ });
+ });
+ const exposureEdges = [...exposures.values()].flatMap((n): Edge[] => {
+ return n.data.exposed.flatMap((e): Edge[] => {
+ return [
+ {
+ id: uuidv4(),
+ source: e.serviceId,
+ sourceHandle: ret.nodes.find((n) => n.id === e.serviceId)!.type === "app" ? "ports" : "env_var",
+ target: n.id,
+ targetHandle: "tcp",
+ },
+ {
+ id: uuidv4(),
+ source: n.id,
+ sourceHandle: "subdomain",
+ target: n.data.network!,
+ targetHandle: "subdomain",
+ },
+ ];
+ });
+ });
+ const ingressEdges = [...(serviceGateways || [])].flatMap((n): Edge[] => {
+ return [
+ {
+ id: uuidv4(),
+ source: n.data.https!.serviceId,
+ sourceHandle: "ports",
+ target: n.id,
+ targetHandle: "https",
+ },
+ {
+ id: uuidv4(),
+ source: n.id,
+ sourceHandle: "subdomain",
+ target: n.data.network!,
+ targetHandle: "subdomain",
+ },
+ ];
+ });
+ ret.edges = [...envVarEdges, ...exposureEdges, ...ingressEdges];
+ return ret;
+}
diff --git a/apps/canvas/config/src/graph.ts b/apps/canvas/config/src/graph.ts
new file mode 100644
index 0000000..e8741f9
--- /dev/null
+++ b/apps/canvas/config/src/graph.ts
@@ -0,0 +1,320 @@
+import { z } from "zod";
+import { Node } from "@xyflow/react";
+import { Domain, ServiceType, VolumeType } from "./types.js";
+
+export const serviceAnalyzisSchema = z.object({
+ name: z.string(),
+ location: z.string(),
+ configVars: z.array(
+ z.object({
+ name: z.string(),
+ category: z.enum(["CommandLineFlag", "EnvironmentVariable"]),
+ type: z.optional(z.enum(["String", "Number", "Boolean"])),
+ semanticType: z.optional(
+ z.enum([
+ "EXPANDED_ENV_VAR",
+ "PORT",
+ "FILESYSTEM_PATH",
+ "DATABASE_URL",
+ "SQLITE_PATH",
+ "POSTGRES_URL",
+ "POSTGRES_PASSWORD",
+ "POSTGRES_USER",
+ "POSTGRES_DB",
+ "POSTGRES_PORT",
+ "POSTGRES_HOST",
+ "POSTGRES_SSL",
+ "MONGO_URL",
+ "MONGO_PASSWORD",
+ "MONGO_USER",
+ "MONGO_DB",
+ "MONGO_PORT",
+ "MONGO_HOST",
+ "MONGO_SSL",
+ ]),
+ ),
+ }),
+ ),
+});
+
+export type BoundEnvVar =
+ | {
+ id: string;
+ source: string | null;
+ }
+ | {
+ id: string;
+ source: string | null;
+ name: string;
+ isEditting: boolean;
+ }
+ | {
+ id: string;
+ source: string | null;
+ name: string;
+ alias: string;
+ isEditting: boolean;
+ }
+ | {
+ id: string;
+ source: string | null;
+ portId: string;
+ name: string;
+ alias: string;
+ isEditting: boolean;
+ };
+
+export type EnvVar = {
+ name: string;
+ value: string;
+};
+
+export type InitData = {
+ label: string;
+ envVars: BoundEnvVar[];
+ ports: Port[];
+};
+
+export type NodeData = InitData & {
+ activeField?: string | undefined;
+ state?: string | null;
+};
+
+export type PortConnectedTo = {
+ serviceId: string;
+ portId: string;
+};
+
+export type NetworkData = NodeData & {
+ domain: string;
+};
+
+export type NetworkNode = Node<NetworkData> & {
+ type: "network";
+};
+
+export type GatewayHttpsData = NodeData & {
+ readonly?: boolean;
+ network?: string;
+ subdomain?: string;
+ https?: PortConnectedTo;
+ auth?: {
+ enabled: boolean;
+ groups: string[];
+ noAuthPathPatterns: string[];
+ };
+};
+
+export type GatewayHttpsNode = Node<GatewayHttpsData> & {
+ type: "gateway-https";
+};
+
+export type GatewayTCPData = NodeData & {
+ readonly?: boolean;
+ network?: string;
+ subdomain?: string;
+ exposed: PortConnectedTo[];
+ selected?: {
+ serviceId?: string;
+ portId?: string;
+ };
+};
+
+export type GatewayTCPNode = Node<GatewayTCPData> & {
+ type: "gateway-tcp";
+};
+
+export type Port = {
+ id: string;
+ name: string;
+ value: number;
+};
+
+export type ServiceData = NodeData & {
+ type: ServiceType;
+ repository?:
+ | {
+ id: number;
+ repoNodeId: string;
+ }
+ | {
+ id: number;
+ repoNodeId: string;
+ branch: string;
+ }
+ | {
+ id: number;
+ repoNodeId: string;
+ branch: string;
+ rootDir: string;
+ };
+ env: string[];
+ volume: string[];
+ preBuildCommands: string;
+ isChoosingPortToConnect: boolean;
+ dev?:
+ | {
+ enabled: false;
+ expose?: Domain;
+ }
+ | {
+ enabled: true;
+ expose?: Domain;
+ codeServerNodeId: string;
+ sshNodeId: string;
+ };
+ info?: z.infer<typeof serviceAnalyzisSchema>;
+};
+
+export type ServiceNode = Node<ServiceData> & {
+ type: "app";
+};
+
+export type VolumeData = NodeData & {
+ type: VolumeType;
+ size: string;
+ attachedTo: string[];
+};
+
+export type VolumeNode = Node<VolumeData> & {
+ type: "volume";
+};
+
+export type PostgreSQLData = NodeData & {
+ volumeId: string;
+};
+
+export type PostgreSQLNode = Node<PostgreSQLData> & {
+ type: "postgresql";
+};
+
+export type MongoDBData = NodeData & {
+ volumeId: string;
+};
+
+export type MongoDBNode = Node<MongoDBData> & {
+ type: "mongodb";
+};
+
+export type GithubData = NodeData & {
+ repository?: {
+ id: number;
+ sshURL: string;
+ fullName: string;
+ };
+};
+
+export type GithubNode = Node<GithubData> & {
+ type: "github";
+};
+
+export type NANode = Node<NodeData> & {
+ type: undefined;
+};
+
+export type AppNode =
+ | NetworkNode
+ | GatewayHttpsNode
+ | GatewayTCPNode
+ | ServiceNode
+ | VolumeNode
+ | PostgreSQLNode
+ | MongoDBNode
+ | GithubNode
+ | NANode;
+
+export type NodeType = Exclude<Pick<AppNode, "type">["type"], undefined>;
+
+export const networkSchema = z.object({
+ name: z.string().min(1),
+ domain: z.string().min(1),
+ hasAuth: z.boolean(),
+});
+
+export type Network = z.infer<typeof networkSchema>;
+
+export const accessSchema = z.discriminatedUnion("type", [
+ z.object({
+ type: z.literal("https"),
+ name: z.string(),
+ address: z.string(),
+ }),
+ z.object({
+ type: z.literal("ssh"),
+ name: z.string(),
+ host: z.string(),
+ port: z.number(),
+ }),
+ z.object({
+ type: z.literal("tcp"),
+ name: z.string(),
+ host: z.string(),
+ port: z.number(),
+ }),
+ z.object({
+ type: z.literal("udp"),
+ name: z.string(),
+ host: z.string(),
+ port: z.number(),
+ }),
+ z.object({
+ type: z.literal("postgresql"),
+ name: z.string(),
+ host: z.string(),
+ port: z.number(),
+ database: z.string(),
+ username: z.string(),
+ password: z.string(),
+ }),
+ z.object({
+ type: z.literal("mongodb"),
+ name: z.string(),
+ host: z.string(),
+ port: z.number(),
+ database: z.string(),
+ username: z.string(),
+ password: z.string(),
+ }),
+]);
+
+export const serviceInfoSchema = z.object({
+ name: z.string(),
+ workers: z.array(
+ z.object({
+ id: z.string(),
+ commit: z.optional(
+ z.object({
+ hash: z.string(),
+ message: z.string(),
+ }),
+ ),
+ commands: z.optional(
+ z.array(
+ z.object({
+ command: z.string(),
+ state: z.string(),
+ }),
+ ),
+ ),
+ }),
+ ),
+});
+
+export const envSchema = z.object({
+ managerAddr: z.optional(z.string().min(1)),
+ instanceId: z.optional(z.string().min(1)),
+ deployKeyPublic: z.optional(z.nullable(z.string().min(1))),
+ networks: z.array(networkSchema).default([]),
+ integrations: z.object({
+ github: z.boolean(),
+ }),
+ services: z.array(serviceInfoSchema),
+ user: z.object({
+ id: z.string(),
+ username: z.string(),
+ }),
+ access: z.array(accessSchema),
+});
+
+export type ServiceInfo = z.infer<typeof serviceInfoSchema>;
+export type Env = z.infer<typeof envSchema>;
diff --git a/apps/canvas/config/src/index.ts b/apps/canvas/config/src/index.ts
new file mode 100644
index 0000000..d064f1c
--- /dev/null
+++ b/apps/canvas/config/src/index.ts
@@ -0,0 +1,51 @@
+export {
+ Auth,
+ AuthDisabled,
+ AuthEnabled,
+ Config,
+ ConfigSchema,
+ ConfigWithInputSchema,
+ Domain,
+ Ingress,
+ MongoDB,
+ PortDomain,
+ PortValue,
+ PostgreSQL,
+ Service,
+ ServiceTypes,
+ Volume,
+ ConfigWithInput,
+ VolumeType,
+} from "./types.js";
+
+export {
+ AppNode,
+ NodeType,
+ Network,
+ ServiceNode,
+ BoundEnvVar,
+ GatewayTCPNode,
+ GatewayHttpsNode,
+ GithubNode,
+ serviceAnalyzisSchema,
+ ServiceData,
+ VolumeNode,
+ PostgreSQLNode,
+ MongoDBNode,
+ Port,
+ EnvVar,
+ NodeData,
+ InitData,
+ NetworkData,
+ GatewayHttpsData,
+ GatewayTCPData,
+ ServiceInfo,
+ Env,
+ VolumeData,
+ PostgreSQLData,
+ MongoDBData,
+ GithubData,
+ envSchema,
+} from "./graph.js";
+
+export { generateDodoConfig, configToGraph } from "./config.js";
diff --git a/apps/canvas/config/src/types.ts b/apps/canvas/config/src/types.ts
new file mode 100644
index 0000000..fe417b8
--- /dev/null
+++ b/apps/canvas/config/src/types.ts
@@ -0,0 +1,155 @@
+import { z } from "zod";
+
+const AuthDisabledSchema = z.object({
+ enabled: z.literal(false),
+});
+
+const AuthEnabledSchema = z.object({
+ enabled: z.literal(true),
+ groups: z.array(z.string()),
+ noAuthPathPatterns: z.array(z.string()),
+});
+
+const AuthSchema = z.union([AuthDisabledSchema, AuthEnabledSchema]);
+
+const IngressSchema = z.object({
+ nodeId: z.string().optional(),
+ network: z.string(),
+ subdomain: z.string(),
+ port: z.union([z.object({ name: z.string() }), z.object({ value: z.string() })]),
+ auth: AuthSchema,
+});
+
+const DomainSchema = z.object({
+ nodeId: z.string().optional(),
+ network: z.string(),
+ subdomain: z.string(),
+});
+
+const PortValueSchema = z.union([
+ z.object({
+ name: z.string(),
+ }),
+ z.object({
+ value: z.number(),
+ }),
+]);
+
+const PortDomainSchema = DomainSchema.extend({
+ port: PortValueSchema,
+});
+
+export const ServiceTypes = [
+ "deno:2.2.0",
+ "golang:1.20.0",
+ "golang:1.22.0",
+ "golang:1.24.0",
+ "hugo:latest",
+ "php:8.2-apache",
+ "nextjs:deno-2.0.0",
+ "nodejs:23.1.0",
+ "nodejs:24.0.2",
+] as const;
+
+const ServiceTypeSchema = z.enum(ServiceTypes);
+
+const ServiceSchema = z.object({
+ nodeId: z.string().optional(),
+ type: ServiceTypeSchema,
+ name: z.string(),
+ source: z.object({
+ repository: z.string(),
+ branch: z.string(),
+ rootDir: z.string(),
+ }),
+ ports: z
+ .array(
+ z.object({
+ name: z.string(),
+ value: z.number(),
+ protocol: z.enum(["TCP", "UDP"]),
+ }),
+ )
+ .optional(),
+ env: z
+ .array(
+ z.object({
+ name: z.string(),
+ alias: z.string().optional(),
+ }),
+ )
+ .optional(),
+ ingress: z.array(IngressSchema).optional(),
+ expose: z.array(PortDomainSchema).optional(),
+ volume: z.array(z.string()).optional(),
+ preBuildCommands: z.array(z.object({ bin: z.string() })).optional(),
+ dev: z
+ .object({
+ enabled: z.boolean(),
+ username: z.string().optional(),
+ ssh: DomainSchema.optional(),
+ codeServer: DomainSchema.optional(),
+ })
+ .optional(),
+});
+
+const VolumeTypeSchema = z.enum(["ReadWriteOnce", "ReadOnlyMany", "ReadWriteMany", "ReadWriteOncePod"]);
+
+const VolumeSchema = z.object({
+ nodeId: z.string().optional(),
+ name: z.string(),
+ size: z.string(),
+ accessMode: VolumeTypeSchema,
+});
+
+const PostgreSQLSchema = z.object({
+ nodeId: z.string().optional(),
+ name: z.string(),
+ size: z.string(),
+ expose: z.array(DomainSchema).optional(),
+});
+
+const MongoDBSchema = z.object({
+ nodeId: z.string().optional(),
+ name: z.string(),
+ size: z.string(),
+ expose: z.array(DomainSchema).optional(),
+});
+
+export const ConfigSchema = z.object({
+ service: z.array(ServiceSchema).optional(),
+ volume: z.array(VolumeSchema).optional(),
+ postgresql: z.array(PostgreSQLSchema).optional(),
+ mongodb: z.array(MongoDBSchema).optional(),
+});
+
+export const InputSchema = z.object({
+ appId: z.string(),
+ managerAddr: z.string(),
+ key: z
+ .object({
+ public: z.string(),
+ private: z.string(),
+ })
+ .optional(),
+});
+
+export const ConfigWithInputSchema = ConfigSchema.extend({
+ input: InputSchema,
+});
+
+export type AuthDisabled = z.infer<typeof AuthDisabledSchema>;
+export type AuthEnabled = z.infer<typeof AuthEnabledSchema>;
+export type Auth = z.infer<typeof AuthSchema>;
+export type Ingress = z.infer<typeof IngressSchema>;
+export type Domain = z.infer<typeof DomainSchema>;
+export type PortValue = z.infer<typeof PortValueSchema>;
+export type PortDomain = z.infer<typeof PortDomainSchema>;
+export type ServiceType = z.infer<typeof ServiceTypeSchema>;
+export type Service = z.infer<typeof ServiceSchema>;
+export type VolumeType = z.infer<typeof VolumeTypeSchema>;
+export type Volume = z.infer<typeof VolumeSchema>;
+export type PostgreSQL = z.infer<typeof PostgreSQLSchema>;
+export type MongoDB = z.infer<typeof MongoDBSchema>;
+export type Config = z.infer<typeof ConfigSchema>;
+export type ConfigWithInput = z.infer<typeof ConfigWithInputSchema>;
diff --git a/apps/canvas/config/tsconfig.json b/apps/canvas/config/tsconfig.json
new file mode 100644
index 0000000..6d6881e
--- /dev/null
+++ b/apps/canvas/config/tsconfig.json
@@ -0,0 +1,105 @@
+{
+ "include": [
+ "src/**/*.ts"
+ ],
+ "compilerOptions": {
+ /* Visit https://aka.ms/tsconfig to read more about this file */
+ "outDir": "dist",
+ /* Projects */
+ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
+ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
+ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
+ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
+ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
+ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
+ /* Language and Environment */
+ "target": "es2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
+ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
+ // "jsx": "preserve", /* Specify what JSX code is generated. */
+ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
+ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
+ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
+ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
+ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
+ // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
+ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
+ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
+ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
+ /* Modules */
+ "module": "node16" /* Specify what module code is generated. */,
+ // "rootDir": "./", /* Specify the root folder within your source files. */
+ "moduleResolution": "node16", /* Specify how TypeScript looks up a file from a given module specifier. */
+ "baseUrl": ".", /* Specify the base directory to resolve non-relative module names. */
+ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
+ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
+ // "types": [], /* Specify type package names to be included without being referenced in a source file. */
+ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
+ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
+ // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
+ // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
+ // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
+ // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
+ // "noUncheckedSideEffectImports": true, /* Check side effect imports. */
+ // "resolveJsonModule": true, /* Enable importing .json files. */
+ // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
+ // "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
+ /* JavaScript Support */
+ // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
+ // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
+ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
+ /* Emit */
+ "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
+ // "declarationMap": true, /* Create sourcemaps for d.ts files. */
+ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
+ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
+ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
+ // "noEmit": true, /* Disable emitting files from a compilation. */
+ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
+ // "outDir": "./", /* Specify an output folder for all emitted files. */
+ // "removeComments": true, /* Disable emitting comments. */
+ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
+ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
+ // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
+ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
+ // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
+ // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
+ // "newLine": "crlf", /* Set the newline character for emitting files. */
+ // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
+ // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
+ // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
+ // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
+ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
+ /* Interop Constraints */
+ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
+ // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
+ // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
+ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
+ "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
+ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
+ "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
+ /* Type Checking */
+ "strict": true /* Enable all strict type-checking options. */,
+ // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
+ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
+ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
+ // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
+ // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
+ // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
+ // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
+ // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
+ // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
+ // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
+ // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
+ // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
+ // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
+ // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
+ // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
+ // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
+ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
+ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
+ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
+ /* Completeness */
+ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
+ "skipLibCheck": true /* Skip type checking all .d.ts files. */
+ }
+}
\ No newline at end of file
diff --git a/apps/canvas/front/package-lock.json b/apps/canvas/front/package-lock.json
index cb422f6..0855cc5 100644
--- a/apps/canvas/front/package-lock.json
+++ b/apps/canvas/front/package-lock.json
@@ -29,6 +29,7 @@
"@xyflow/react": "^12.3.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
+ "config": "file:../config",
"lucide-react": "^0.454.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -61,6 +62,20 @@
"watch": "^1.0.2"
}
},
+ "../config": {
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "@xyflow/react": "^12.3.3",
+ "zod": "^3.24.4"
+ },
+ "devDependencies": {
+ "eslint": "^9.13.0",
+ "prettier": "3.5.3",
+ "typescript": "^5.8.3",
+ "typescript-eslint": "^8.11.0"
+ }
+ },
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
@@ -4656,6 +4671,10 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
+ "node_modules/config": {
+ "resolved": "../config",
+ "link": true
+ },
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
diff --git a/apps/canvas/front/package.json b/apps/canvas/front/package.json
index 43d1232..0b31f98 100644
--- a/apps/canvas/front/package.json
+++ b/apps/canvas/front/package.json
@@ -37,6 +37,7 @@
"@xyflow/react": "^12.3.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
+ "config": "file:../config",
"lucide-react": "^0.454.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
diff --git a/apps/canvas/front/src/Config.tsx b/apps/canvas/front/src/Config.tsx
index bdad346..eb024ea 100644
--- a/apps/canvas/front/src/Config.tsx
+++ b/apps/canvas/front/src/Config.tsx
@@ -1,5 +1,5 @@
import { useStateStore } from "./lib/state";
-import { generateDodoConfig } from "./lib/config";
+import { generateDodoConfig } from "../../config/src/config";
import JSONView from "@microlink/react-json-view";
import { useMemo } from "react";
diff --git a/apps/canvas/front/src/components/actions.tsx b/apps/canvas/front/src/components/actions.tsx
index d4bd5ea..1442b9a 100644
--- a/apps/canvas/front/src/components/actions.tsx
+++ b/apps/canvas/front/src/components/actions.tsx
@@ -1,7 +1,7 @@
-import { AppNode, nodeLabelFull, useEnv, useMessages, useProjectId, useStateStore } from "@/lib/state";
+import { nodeLabelFull, useEnv, useMessages, useProjectId, useStateStore } from "@/lib/state";
import { Button } from "./ui/button";
import { useCallback, useEffect, useState } from "react";
-import { generateDodoConfig } from "@/lib/config";
+import { generateDodoConfig, AppNode } from "config";
import { useNodes, useReactFlow } from "@xyflow/react";
import { useToast } from "@/hooks/use-toast";
import {
@@ -148,9 +148,9 @@
method: "GET",
});
const inst = await resp.json();
- const { x = 0, y = 0, zoom = 1 } = inst.viewport;
- store.setNodes(inst.nodes || []);
- store.setEdges(inst.edges || []);
+ const { x = 0, y = 0, zoom = 1 } = inst.state.viewport;
+ store.setNodes(inst.state.nodes || []);
+ store.setEdges(inst.state.edges || []);
instance.setViewport({ x, y, zoom });
}, [projectId, instance, store]);
const clear = useCallback(() => {
diff --git a/apps/canvas/front/src/components/import-modal.tsx b/apps/canvas/front/src/components/import-modal.tsx
index ea9a06c..44f66d5 100644
--- a/apps/canvas/front/src/components/import-modal.tsx
+++ b/apps/canvas/front/src/components/import-modal.tsx
@@ -11,9 +11,6 @@
useGithubRepositoriesLoading,
useGithubRepositoriesError,
useFetchGithubRepositories,
- serviceAnalyzisSchema,
- ServiceType,
- ServiceData,
useStateStore,
} from "@/lib/state";
import { Alert, AlertDescription } from "./ui/alert";
@@ -24,6 +21,7 @@
import { Switch } from "./ui/switch";
import { Label } from "./ui/label";
import { useToast } from "@/hooks/use-toast";
+import { serviceAnalyzisSchema, ServiceType, ServiceData } from "config";
const schema = z.object({
repositoryId: z.number().optional(),
diff --git a/apps/canvas/front/src/components/node-app.tsx b/apps/canvas/front/src/components/node-app.tsx
index d9eea6d..7eb632c 100644
--- a/apps/canvas/front/src/components/node-app.tsx
+++ b/apps/canvas/front/src/components/node-app.tsx
@@ -1,20 +1,7 @@
import { v4 as uuidv4 } from "uuid";
import { NodeRect } from "./node-rect";
-import {
- useStateStore,
- ServiceNode,
- ServiceTypes,
- nodeLabel,
- BoundEnvVar,
- AppState,
- nodeIsConnectable,
- GatewayTCPNode,
- GatewayHttpsNode,
- AppNode,
- GithubNode,
- useEnv,
- useGithubRepositories,
-} from "@/lib/state";
+import { useStateStore, nodeLabel, AppState, nodeIsConnectable, useEnv, useGithubRepositories } from "@/lib/state";
+import { ServiceNode, ServiceTypes } from "config";
import { KeyboardEvent, FocusEvent, useCallback, useEffect, useMemo, useState } from "react";
import { z } from "zod";
import { useForm, EventType, DeepPartial } from "react-hook-form";
diff --git a/apps/canvas/front/src/lib/config.ts b/apps/canvas/front/src/lib/config.ts
index 513f1f0..39db5b4 100644
--- a/apps/canvas/front/src/lib/config.ts
+++ b/apps/canvas/front/src/lib/config.ts
@@ -1,235 +1,5 @@
-import { AppNode, Env, Message, MessageType, NodeType, ServiceType, VolumeType } from "./state";
-
-export type AuthDisabled = {
- enabled: false;
-};
-
-export type AuthEnabled = {
- enabled: true;
- groups: string[];
- noAuthPathPatterns: string[];
-};
-
-export type Auth = AuthDisabled | AuthEnabled;
-
-export type Ingress = {
- network: string;
- subdomain: string;
- port: { name: string } | { value: string };
- auth: Auth;
-};
-
-export type Domain = {
- network: string;
- subdomain: string;
-};
-
-export type PortValue =
- | {
- name: string;
- }
- | {
- value: number;
- };
-
-export type PortDomain = Domain & {
- port: PortValue;
-};
-
-export type Service = {
- type: ServiceType;
- name: string;
- source: {
- repository: string;
- branch: string;
- rootDir: string;
- };
- ports?: {
- name: string;
- value: number;
- protocol: "TCP" | "UDP";
- }[];
- env?: {
- name: string;
- alias?: string;
- }[];
- ingress?: Ingress[];
- expose?: PortDomain[];
- volume?: string[];
- preBuildCommands?: { bin: string }[];
- dev?: {
- enabled: boolean;
- username?: string;
- ssh?: Domain;
- codeServer?: Domain;
- };
-};
-
-export type Volume = {
- name: string;
- accessMode: VolumeType;
- size: string;
-};
-
-export type PostgreSQL = {
- name: string;
- size: string;
- expose?: Domain[];
-};
-
-export type MongoDB = {
- name: string;
- size: string;
- expose?: Domain[];
-};
-
-export type Config = {
- input: {
- appId: string;
- managerAddr: string;
- };
- service?: Service[];
- volume?: Volume[];
- postgresql?: PostgreSQL[];
- mongodb?: MongoDB[];
-};
-
-export function generateDodoConfig(appId: string | undefined, nodes: AppNode[], env: Env): Config | null {
- try {
- if (appId == null || env.managerAddr == null) {
- return null;
- }
- const networkMap = new Map(env.networks.map((n) => [n.domain, n.name]));
- const ingressNodes = nodes
- .filter((n) => n.type === "gateway-https")
- .filter((n) => n.data.https !== undefined && !n.data.readonly);
- const tcpNodes = nodes
- .filter((n) => n.type === "gateway-tcp")
- .filter((n) => n.data.exposed !== undefined && !n.data.readonly);
- const findExpose = (n: AppNode): PortDomain[] => {
- return n.data.ports
- .map((p) => [n.id, p.id, p.name])
- .flatMap((sp) => {
- return tcpNodes.flatMap((i) =>
- (i.data.exposed || [])
- .filter((t) => t.serviceId === sp[0] && t.portId === sp[1])
- .map(() => ({
- network: networkMap.get(i.data.network!)!,
- subdomain: i.data.subdomain!,
- port: { name: sp[2] },
- })),
- );
- });
- };
- return {
- input: {
- appId: appId,
- managerAddr: env.managerAddr,
- },
- service: nodes
- .filter((n) => n.type === "app")
- .map((n): Service => {
- return {
- type: n.data.type,
- name: n.data.label,
- source: {
- repository: nodes
- .filter((i) => i.type === "github")
- .find((i) => i.id === n.data.repository.id)!.data.repository!.sshURL,
- branch: n.data.repository.branch,
- rootDir: n.data.repository.rootDir,
- },
- ports: (n.data.ports || [])
- .filter((p) => !n.data.dev?.enabled || (p.value != 22 && p.value != 9090))
- .map((p) => ({
- name: p.name,
- value: p.value,
- protocol: "TCP", // TODO(gio)
- })),
- env: (n.data.envVars || [])
- .filter((e) => "name" in e)
- .map((e) => ({
- name: e.name,
- alias: "alias" in e ? e.alias : undefined,
- })),
- ingress: ingressNodes
- .filter((i) => i.data.https!.serviceId === n.id)
- .map(
- (i): Ingress => ({
- network: networkMap.get(i.data.network!)!,
- subdomain: i.data.subdomain!,
- port: {
- name: n.data.ports.find((p) => p.id === i.data.https!.portId)!.name,
- },
- auth:
- i.data.auth?.enabled || false
- ? {
- enabled: true,
- groups: i.data.auth!.groups,
- noAuthPathPatterns: i.data.auth!.noAuthPathPatterns,
- }
- : {
- enabled: false,
- },
- }),
- ),
- expose: findExpose(n),
- preBuildCommands: n.data.preBuildCommands
- ? n.data.preBuildCommands.split("\n").map((cmd) => ({ bin: cmd }))
- : [],
- dev: {
- enabled: n.data.dev ? n.data.dev.enabled : false,
- username: n.data.dev && n.data.dev.enabled ? env.user.username : undefined,
- codeServer:
- n.data.dev?.enabled && n.data.dev.expose != null
- ? {
- network: networkMap.get(n.data.dev.expose.network)!,
- subdomain: n.data.dev.expose.subdomain,
- }
- : undefined,
- ssh:
- n.data.dev?.enabled && n.data.dev.expose != null
- ? {
- network: networkMap.get(n.data.dev.expose.network)!,
- subdomain: n.data.dev.expose.subdomain,
- }
- : undefined,
- },
- };
- }),
- volume: nodes
- .filter((n) => n.type === "volume")
- .map(
- (n): Volume => ({
- name: n.data.label,
- accessMode: n.data.type,
- size: n.data.size,
- }),
- ),
- postgresql: nodes
- .filter((n) => n.type === "postgresql")
- .map(
- (n): PostgreSQL => ({
- name: n.data.label,
- size: "1Gi", // TODO(gio)
- expose: findExpose(n).map((e) => ({ network: e.network, subdomain: e.subdomain })),
- }),
- ),
- mongodb: nodes
- .filter((n) => n.type === "mongodb")
- .map(
- (n): MongoDB => ({
- name: n.data.label,
- size: "1Gi", // TODO(gio)
- expose: findExpose(n).map((e) => ({ network: e.network, subdomain: e.subdomain })),
- }),
- ),
- };
- } catch (e) {
- console.log(e);
- return null;
- }
-}
+import { AppNode, NodeType } from "config";
+import { Message, MessageType } from "./state";
export interface Validator {
(nodes: AppNode[]): Message[];
@@ -347,7 +117,7 @@
}) satisfies Message,
);
const noApp = git
- .filter((n) => !nodes.some((i) => i.type === "app" && i.data?.repository?.id === n.id))
+ .filter((n) => !nodes.some((i) => i.type === "app" && i.data?.repository?.repoNodeId === n.id))
.map(
(n) =>
({
@@ -383,7 +153,7 @@
}),
);
const noSource = apps
- .filter((n) => n.data == null || n.data.repository == null || n.data.repository.id === "")
+ .filter((n) => n.data == null || n.data.repository == null || n.data.repository.repoNodeId === "")
.map(
(n): Message => ({
id: `${n.id}-no-repo`,
diff --git a/apps/canvas/front/src/lib/state.ts b/apps/canvas/front/src/lib/state.ts
index a0e4e91..8ada938 100644
--- a/apps/canvas/front/src/lib/state.ts
+++ b/apps/canvas/front/src/lib/state.ts
@@ -1,7 +1,7 @@
import { Category, defaultCategories } from "./categories";
import { CreateValidators, Validator } from "./config";
import { GitHubService, GitHubServiceImpl, GitHubRepository } from "./github";
-import type { Edge, Node, OnConnect, OnEdgesChange, OnNodesChange, Viewport as ReactFlowViewport } from "@xyflow/react";
+import type { Edge, OnConnect, OnEdgesChange, OnNodesChange, Viewport as ReactFlowViewport } from "@xyflow/react";
import {
addEdge,
applyEdgeChanges,
@@ -13,217 +13,8 @@
} from "@xyflow/react";
import type { DeepPartial } from "react-hook-form";
import { v4 as uuidv4 } from "uuid";
-import { z } from "zod";
import { create } from "zustand";
-
-export const serviceAnalyzisSchema = z.object({
- name: z.string(),
- location: z.string(),
- configVars: z.array(
- z.object({
- name: z.string(),
- category: z.enum(["CommandLineFlag", "EnvironmentVariable"]),
- type: z.optional(z.enum(["String", "Number", "Boolean"])),
- semanticType: z.optional(
- z.enum([
- "EXPANDED_ENV_VAR",
- "PORT",
- "FILESYSTEM_PATH",
- "DATABASE_URL",
- "SQLITE_PATH",
- "POSTGRES_URL",
- "POSTGRES_PASSWORD",
- "POSTGRES_USER",
- "POSTGRES_DB",
- "POSTGRES_PORT",
- "POSTGRES_HOST",
- "POSTGRES_SSL",
- "MONGO_URL",
- "MONGO_PASSWORD",
- "MONGO_USER",
- "MONGO_DB",
- "MONGO_PORT",
- "MONGO_HOST",
- "MONGO_SSL",
- ]),
- ),
- }),
- ),
-});
-
-export type InitData = {
- label: string;
- envVars: BoundEnvVar[];
- ports: Port[];
-};
-
-export type NodeData = InitData & {
- activeField?: string | undefined;
- state?: string | null;
-};
-
-export type PortConnectedTo = {
- serviceId: string;
- portId: string;
-};
-
-export type NetworkData = NodeData & {
- domain: string;
-};
-
-export type NetworkNode = Node<NetworkData> & {
- type: "network";
-};
-
-export type GatewayHttpsData = NodeData & {
- readonly?: boolean;
- network?: string;
- subdomain?: string;
- https?: PortConnectedTo;
- auth?: {
- enabled: boolean;
- groups: string[];
- noAuthPathPatterns: string[];
- };
-};
-
-export type GatewayHttpsNode = Node<GatewayHttpsData> & {
- type: "gateway-https";
-};
-
-export type GatewayTCPData = NodeData & {
- readonly?: boolean;
- network?: string;
- subdomain?: string;
- exposed: PortConnectedTo[];
- selected?: {
- serviceId?: string;
- portId?: string;
- };
-};
-
-export type GatewayTCPNode = Node<GatewayTCPData> & {
- type: "gateway-tcp";
-};
-
-export type Port = {
- id: string;
- name: string;
- value: number;
-};
-
-export const ServiceTypes = [
- "deno:2.2.0",
- "golang:1.20.0",
- "golang:1.22.0",
- "golang:1.24.0",
- "hugo:latest",
- "php:8.2-apache",
- "nextjs:deno-2.0.0",
- "nodejs:23.1.0",
- "nodejs:24.0.2",
-] as const;
-export type ServiceType = (typeof ServiceTypes)[number];
-
-export type Domain = {
- network: string;
- subdomain: string;
-};
-
-export type ServiceData = NodeData & {
- type: ServiceType;
- repository?:
- | {
- id: number;
- repoNodeId: string;
- }
- | {
- id: number;
- repoNodeId: string;
- branch: string;
- }
- | {
- id: number;
- repoNodeId: string;
- branch: string;
- rootDir: string;
- };
- env: string[];
- volume: string[];
- preBuildCommands: string;
- isChoosingPortToConnect: boolean;
- dev?:
- | {
- enabled: false;
- expose?: Domain;
- }
- | {
- enabled: true;
- expose?: Domain;
- codeServerNodeId: string;
- sshNodeId: string;
- };
- info?: z.infer<typeof serviceAnalyzisSchema>;
-};
-
-export type ServiceNode = Node<ServiceData> & {
- type: "app";
-};
-
-export type VolumeType = "ReadWriteOnce" | "ReadOnlyMany" | "ReadWriteMany" | "ReadWriteOncePod";
-
-export type VolumeData = NodeData & {
- type: VolumeType;
- size: string;
- attachedTo: string[];
-};
-
-export type VolumeNode = Node<VolumeData> & {
- type: "volume";
-};
-
-export type PostgreSQLData = NodeData & {
- volumeId: string;
-};
-
-export type PostgreSQLNode = Node<PostgreSQLData> & {
- type: "postgresql";
-};
-
-export type MongoDBData = NodeData & {
- volumeId: string;
-};
-
-export type MongoDBNode = Node<MongoDBData> & {
- type: "mongodb";
-};
-
-export type GithubData = NodeData & {
- repository?: {
- id: number;
- sshURL: string;
- fullName: string;
- };
-};
-
-export type GithubNode = Node<GithubData> & {
- type: "github";
-};
-
-export type NANode = Node<NodeData> & {
- type: undefined;
-};
-
-export type AppNode =
- | NetworkNode
- | GatewayHttpsNode
- | GatewayTCPNode
- | ServiceNode
- | VolumeNode
- | PostgreSQLNode
- | MongoDBNode
- | GithubNode
- | NANode;
+import { AppNode, Env, NodeType, VolumeNode, GatewayTCPData, envSchema } from "config";
export function nodeLabel(n: AppNode): string {
try {
@@ -319,38 +110,6 @@
}
}
-export type BoundEnvVar =
- | {
- id: string;
- source: string | null;
- }
- | {
- id: string;
- source: string | null;
- name: string;
- isEditting: boolean;
- }
- | {
- id: string;
- source: string | null;
- name: string;
- alias: string;
- isEditting: boolean;
- }
- | {
- id: string;
- source: string | null;
- portId: string;
- name: string;
- alias: string;
- isEditting: boolean;
- };
-
-export type EnvVar = {
- name: string;
- value: string;
-};
-
export function nodeEnvVarNamePort(n: AppNode, portName: string): string {
return `DODO_SERVICE_${n.data.label.toUpperCase()}_ADDRESS_${portName.toUpperCase()}`;
}
@@ -381,8 +140,6 @@
}
}
-export type NodeType = Exclude<Pick<AppNode, "type">["type"], undefined>;
-
export type MessageType = "INFO" | "WARNING" | "FATAL";
export type Message = {
@@ -395,100 +152,6 @@
onClick?: (state: AppState) => void;
};
-export const accessSchema = z.discriminatedUnion("type", [
- z.object({
- type: z.literal("https"),
- name: z.string(),
- address: z.string(),
- }),
- z.object({
- type: z.literal("ssh"),
- name: z.string(),
- host: z.string(),
- port: z.number(),
- }),
- z.object({
- type: z.literal("tcp"),
- name: z.string(),
- host: z.string(),
- port: z.number(),
- }),
- z.object({
- type: z.literal("udp"),
- name: z.string(),
- host: z.string(),
- port: z.number(),
- }),
- z.object({
- type: z.literal("postgresql"),
- name: z.string(),
- host: z.string(),
- port: z.number(),
- database: z.string(),
- username: z.string(),
- password: z.string(),
- }),
- z.object({
- type: z.literal("mongodb"),
- name: z.string(),
- host: z.string(),
- port: z.number(),
- database: z.string(),
- username: z.string(),
- password: z.string(),
- }),
-]);
-
-export const serviceInfoSchema = z.object({
- name: z.string(),
- workers: z.array(
- z.object({
- id: z.string(),
- commit: z.optional(
- z.object({
- hash: z.string(),
- message: z.string(),
- }),
- ),
- commands: z.optional(
- z.array(
- z.object({
- command: z.string(),
- state: z.string(),
- }),
- ),
- ),
- }),
- ),
-});
-
-export const envSchema = z.object({
- managerAddr: z.optional(z.string().min(1)),
- instanceId: z.optional(z.string().min(1)),
- deployKeyPublic: z.optional(z.nullable(z.string().min(1))),
- networks: z
- .array(
- z.object({
- name: z.string().min(1),
- domain: z.string().min(1),
- hasAuth: z.boolean(),
- }),
- )
- .default([]),
- integrations: z.object({
- github: z.boolean(),
- }),
- services: z.array(serviceInfoSchema),
- user: z.object({
- id: z.string(),
- username: z.string(),
- }),
- access: z.array(accessSchema),
-});
-
-export type ServiceInfo = z.infer<typeof serviceInfoSchema>;
-export type Env = z.infer<typeof envSchema>;
-
const defaultEnv: Env = {
managerAddr: undefined,
deployKeyPublic: undefined,
@@ -729,15 +392,15 @@
method: "GET",
});
const inst = await resp.json();
- setN(inst.nodes);
- set({ edges: inst.edges });
+ setN(inst.state.nodes);
+ set({ edges: inst.state.edges });
injectNetworkNodes();
if (
- get().zoom.x !== inst.viewport.x ||
- get().zoom.y !== inst.viewport.y ||
- get().zoom.zoom !== inst.viewport.zoom
+ get().zoom.x !== inst.state.viewport.x ||
+ get().zoom.y !== inst.state.viewport.y ||
+ get().zoom.zoom !== inst.state.viewport.zoom
) {
- set({ zoom: inst.viewport });
+ set({ zoom: inst.state.viewport });
}
};
diff --git a/apps/canvas/front/tsconfig.app.json b/apps/canvas/front/tsconfig.app.json
index 6c2d8de..803c90a 100644
--- a/apps/canvas/front/tsconfig.app.json
+++ b/apps/canvas/front/tsconfig.app.json
@@ -27,5 +27,5 @@
"@/*": ["./src/*"]
}
},
- "include": ["src"]
+ "include": ["src", "../config/src/config.ts"]
}