diff --git a/apps/dokploy/__test__/compose/compose.test.ts b/apps/dokploy/__test__/compose/compose.test.ts index 69d3a5212..b691537a1 100644 --- a/apps/dokploy/__test__/compose/compose.test.ts +++ b/apps/dokploy/__test__/compose/compose.test.ts @@ -1,7 +1,7 @@ import type { ComposeSpecification } from "@dokploy/server"; import { addSuffixToAllProperties } from "@dokploy/server"; -import { load } from "js-yaml"; import { expect, test } from "vitest"; +import { parse } from "yaml"; const composeFile1 = ` version: "3.8" @@ -61,7 +61,7 @@ secrets: file: ./db_password.txt `; -const expectedComposeFile1 = load(` +const expectedComposeFile1 = parse(` version: "3.8" services: @@ -120,7 +120,7 @@ secrets: `) as ComposeSpecification; test("Add suffix to all properties in compose file 1", () => { - const composeData = load(composeFile1) as ComposeSpecification; + const composeData = parse(composeFile1) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllProperties(composeData, suffix); @@ -185,7 +185,7 @@ secrets: file: ./db_password.txt `; -const expectedComposeFile2 = load(` +const expectedComposeFile2 = parse(` version: "3.8" services: @@ -243,7 +243,7 @@ secrets: `) as ComposeSpecification; test("Add suffix to all properties in compose file 2", () => { - const composeData = load(composeFile2) as ComposeSpecification; + const composeData = parse(composeFile2) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllProperties(composeData, suffix); @@ -308,7 +308,7 @@ secrets: file: ./service_secret.txt `; -const expectedComposeFile3 = load(` +const expectedComposeFile3 = parse(` version: "3.8" services: @@ -366,7 +366,7 @@ secrets: `) as ComposeSpecification; test("Add suffix to all properties in compose file 3", () => { - const composeData = load(composeFile3) as ComposeSpecification; + const composeData = parse(composeFile3) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllProperties(composeData, suffix); @@ -420,7 +420,7 @@ volumes: driver: local `; -const expectedComposeFile = load(` +const expectedComposeFile = parse(` version: "3.8" services: @@ -467,7 +467,7 @@ volumes: `) as ComposeSpecification; test("Add suffix to all properties in Plausible compose file", () => { - const composeData = load(composeFile) as ComposeSpecification; + const composeData = parse(composeFile) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllProperties(composeData, suffix); diff --git a/apps/dokploy/__test__/compose/config/config-root.test.ts b/apps/dokploy/__test__/compose/config/config-root.test.ts index 668e17902..a633bab53 100644 --- a/apps/dokploy/__test__/compose/config/config-root.test.ts +++ b/apps/dokploy/__test__/compose/config/config-root.test.ts @@ -1,7 +1,7 @@ import type { ComposeSpecification } from "@dokploy/server"; import { addSuffixToConfigsRoot, generateRandomHash } from "@dokploy/server"; -import { load } from "js-yaml"; import { expect, test } from "vitest"; +import { parse } from "yaml"; test("Generate random hash with 8 characters", () => { const hash = generateRandomHash(); @@ -23,7 +23,7 @@ configs: `; test("Add suffix to configs in root property", () => { - const composeData = load(composeFile) as ComposeSpecification; + const composeData = parse(composeFile) as ComposeSpecification; const suffix = generateRandomHash(); @@ -59,7 +59,7 @@ configs: `; test("Add suffix to multiple configs in root property", () => { - const composeData = load(composeFileMultipleConfigs) as ComposeSpecification; + const composeData = parse(composeFileMultipleConfigs) as ComposeSpecification; const suffix = generateRandomHash(); @@ -92,7 +92,7 @@ configs: `; test("Add suffix to configs with different properties in root property", () => { - const composeData = load( + const composeData = parse( composeFileDifferentProperties, ) as ComposeSpecification; @@ -137,7 +137,7 @@ configs: `; // Expected compose file con el prefijo `testhash` -const expectedComposeFileConfigRoot = load(` +const expectedComposeFileConfigRoot = parse(` version: "3.8" services: @@ -162,7 +162,7 @@ configs: `) as ComposeSpecification; test("Add suffix to configs in root property", () => { - const composeData = load(composeFileConfigRoot) as ComposeSpecification; + const composeData = parse(composeFileConfigRoot) as ComposeSpecification; const suffix = "testhash"; diff --git a/apps/dokploy/__test__/compose/config/config-service.test.ts b/apps/dokploy/__test__/compose/config/config-service.test.ts index 246872f09..08dd696e6 100644 --- a/apps/dokploy/__test__/compose/config/config-service.test.ts +++ b/apps/dokploy/__test__/compose/config/config-service.test.ts @@ -3,8 +3,8 @@ import { addSuffixToConfigsInServices, generateRandomHash, } from "@dokploy/server"; -import { load } from "js-yaml"; import { expect, test } from "vitest"; +import { parse } from "yaml"; const composeFile = ` version: "3.8" @@ -22,7 +22,7 @@ configs: `; test("Add suffix to configs in services", () => { - const composeData = load(composeFile) as ComposeSpecification; + const composeData = parse(composeFile) as ComposeSpecification; const suffix = generateRandomHash(); @@ -54,7 +54,7 @@ configs: `; test("Add suffix to configs in services with single config", () => { - const composeData = load( + const composeData = parse( composeFileSingleServiceConfig, ) as ComposeSpecification; @@ -108,7 +108,7 @@ configs: `; test("Add suffix to configs in services with multiple configs", () => { - const composeData = load( + const composeData = parse( composeFileMultipleServicesConfigs, ) as ComposeSpecification; @@ -157,7 +157,7 @@ services: `; // Expected compose file con el prefijo `testhash` -const expectedComposeFileConfigServices = load(` +const expectedComposeFileConfigServices = parse(` version: "3.8" services: @@ -182,7 +182,7 @@ services: `) as ComposeSpecification; test("Add suffix to configs in services", () => { - const composeData = load(composeFileConfigServices) as ComposeSpecification; + const composeData = parse(composeFileConfigServices) as ComposeSpecification; const suffix = "testhash"; diff --git a/apps/dokploy/__test__/compose/config/config.test.ts b/apps/dokploy/__test__/compose/config/config.test.ts index 2d5feeb9a..3a160431e 100644 --- a/apps/dokploy/__test__/compose/config/config.test.ts +++ b/apps/dokploy/__test__/compose/config/config.test.ts @@ -1,7 +1,7 @@ import type { ComposeSpecification } from "@dokploy/server"; import { addSuffixToAllConfigs, generateRandomHash } from "@dokploy/server"; -import { load } from "js-yaml"; import { expect, test } from "vitest"; +import { parse } from "yaml"; test("Generate random hash with 8 characters", () => { const hash = generateRandomHash(); @@ -43,7 +43,7 @@ configs: file: ./db-config.yml `; -const expectedComposeFileCombinedConfigs = load(` +const expectedComposeFileCombinedConfigs = parse(` version: "3.8" services: @@ -77,7 +77,7 @@ configs: `) as ComposeSpecification; test("Add suffix to all configs in root and services", () => { - const composeData = load(composeFileCombinedConfigs) as ComposeSpecification; + const composeData = parse(composeFileCombinedConfigs) as ComposeSpecification; const suffix = "testhash"; @@ -122,7 +122,7 @@ configs: file: ./db-config.yml `; -const expectedComposeFileWithEnvAndExternal = load(` +const expectedComposeFileWithEnvAndExternal = parse(` version: "3.8" services: @@ -159,7 +159,7 @@ configs: `) as ComposeSpecification; test("Add suffix to configs with environment and external", () => { - const composeData = load( + const composeData = parse( composeFileWithEnvAndExternal, ) as ComposeSpecification; @@ -200,7 +200,7 @@ configs: file: ./db-config.yml `; -const expectedComposeFileWithTemplateDriverAndLabels = load(` +const expectedComposeFileWithTemplateDriverAndLabels = parse(` version: "3.8" services: @@ -231,7 +231,7 @@ configs: `) as ComposeSpecification; test("Add suffix to configs with template driver and labels", () => { - const composeData = load( + const composeData = parse( composeFileWithTemplateDriverAndLabels, ) as ComposeSpecification; diff --git a/apps/dokploy/__test__/compose/network/network-root.test.ts b/apps/dokploy/__test__/compose/network/network-root.test.ts index c55f6fa86..0d3c841d4 100644 --- a/apps/dokploy/__test__/compose/network/network-root.test.ts +++ b/apps/dokploy/__test__/compose/network/network-root.test.ts @@ -1,7 +1,7 @@ import type { ComposeSpecification } from "@dokploy/server"; import { addSuffixToNetworksRoot, generateRandomHash } from "@dokploy/server"; -import { load } from "js-yaml"; import { expect, test } from "vitest"; +import { parse } from "yaml"; const composeFile = ` version: "3.8" @@ -35,7 +35,7 @@ test("Generate random hash with 8 characters", () => { }); test("Add suffix to networks root property", () => { - const composeData = load(composeFile) as ComposeSpecification; + const composeData = parse(composeFile) as ComposeSpecification; const suffix = generateRandomHash(); @@ -79,7 +79,7 @@ networks: `; test("Add suffix to advanced networks root property (2 TRY)", () => { - const composeData = load(composeFile2) as ComposeSpecification; + const composeData = parse(composeFile2) as ComposeSpecification; const suffix = generateRandomHash(); @@ -120,7 +120,7 @@ networks: `; test("Add suffix to networks with external properties", () => { - const composeData = load(composeFile3) as ComposeSpecification; + const composeData = parse(composeFile3) as ComposeSpecification; const suffix = generateRandomHash(); @@ -160,7 +160,7 @@ networks: `; test("Add suffix to networks with IPAM configurations", () => { - const composeData = load(composeFile4) as ComposeSpecification; + const composeData = parse(composeFile4) as ComposeSpecification; const suffix = generateRandomHash(); @@ -201,7 +201,7 @@ networks: `; test("Add suffix to networks with custom options", () => { - const composeData = load(composeFile5) as ComposeSpecification; + const composeData = parse(composeFile5) as ComposeSpecification; const suffix = generateRandomHash(); @@ -264,7 +264,7 @@ networks: `; test("Add suffix to networks with static suffix", () => { - const composeData = load(composeFile6) as ComposeSpecification; + const composeData = parse(composeFile6) as ComposeSpecification; const suffix = "testhash"; @@ -273,7 +273,7 @@ test("Add suffix to networks with static suffix", () => { } const networks = addSuffixToNetworksRoot(composeData.networks, suffix); - const expectedComposeData = load( + const expectedComposeData = parse( expectedComposeFile6, ) as ComposeSpecification; expect(networks).toStrictEqual(expectedComposeData.networks); @@ -293,7 +293,7 @@ networks: `; test("It shoudn't add suffix to dokploy-network", () => { - const composeData = load(composeFile7) as ComposeSpecification; + const composeData = parse(composeFile7) as ComposeSpecification; const suffix = generateRandomHash(); diff --git a/apps/dokploy/__test__/compose/network/network-service.test.ts b/apps/dokploy/__test__/compose/network/network-service.test.ts index 3cf46d4ab..e07fa1546 100644 --- a/apps/dokploy/__test__/compose/network/network-service.test.ts +++ b/apps/dokploy/__test__/compose/network/network-service.test.ts @@ -3,8 +3,8 @@ import { addSuffixToServiceNetworks, generateRandomHash, } from "@dokploy/server"; -import { load } from "js-yaml"; import { expect, test } from "vitest"; +import { parse } from "yaml"; const composeFile = ` version: "3.8" @@ -23,7 +23,7 @@ services: `; test("Add suffix to networks in services", () => { - const composeData = load(composeFile) as ComposeSpecification; + const composeData = parse(composeFile) as ComposeSpecification; const suffix = generateRandomHash(); @@ -67,7 +67,7 @@ networks: `; test("Add suffix to networks in services with aliases", () => { - const composeData = load(composeFile2) as ComposeSpecification; + const composeData = parse(composeFile2) as ComposeSpecification; const suffix = generateRandomHash(); @@ -107,7 +107,7 @@ networks: `; test("Add suffix to networks in services (Object with simple networks)", () => { - const composeData = load(composeFile3) as ComposeSpecification; + const composeData = parse(composeFile3) as ComposeSpecification; const suffix = generateRandomHash(); @@ -153,7 +153,7 @@ networks: `; test("Add suffix to networks in services (combined case)", () => { - const composeData = load(composeFileCombined) as ComposeSpecification; + const composeData = parse(composeFileCombined) as ComposeSpecification; const suffix = generateRandomHash(); @@ -196,7 +196,7 @@ services: `; test("It shoudn't add suffix to dokploy-network in services", () => { - const composeData = load(composeFile7) as ComposeSpecification; + const composeData = parse(composeFile7) as ComposeSpecification; const suffix = generateRandomHash(); @@ -245,7 +245,7 @@ services: `; test("It shoudn't add suffix to dokploy-network in services multiples cases", () => { - const composeData = load(composeFile8) as ComposeSpecification; + const composeData = parse(composeFile8) as ComposeSpecification; const suffix = generateRandomHash(); diff --git a/apps/dokploy/__test__/compose/network/network.test.ts b/apps/dokploy/__test__/compose/network/network.test.ts index 7ba1c6a83..c1900ed74 100644 --- a/apps/dokploy/__test__/compose/network/network.test.ts +++ b/apps/dokploy/__test__/compose/network/network.test.ts @@ -5,8 +5,8 @@ import { addSuffixToServiceNetworks, generateRandomHash, } from "@dokploy/server"; -import { load } from "js-yaml"; import { expect, test } from "vitest"; +import { parse } from "yaml"; const composeFileCombined = ` version: "3.8" @@ -39,7 +39,7 @@ networks: `; test("Add suffix to networks in services and root (combined case)", () => { - const composeData = load(composeFileCombined) as ComposeSpecification; + const composeData = parse(composeFileCombined) as ComposeSpecification; const suffix = generateRandomHash(); @@ -89,7 +89,7 @@ test("Add suffix to networks in services and root (combined case)", () => { expect(redisNetworks).not.toHaveProperty("backend"); }); -const expectedComposeFile = load(` +const expectedComposeFile = parse(` version: "3.8" services: @@ -120,7 +120,7 @@ networks: `); test("Add suffix to networks in compose file", () => { - const composeData = load(composeFileCombined) as ComposeSpecification; + const composeData = parse(composeFileCombined) as ComposeSpecification; const suffix = "testhash"; if (!composeData?.networks) { @@ -156,7 +156,7 @@ networks: driver: bridge `; -const expectedComposeFile2 = load(` +const expectedComposeFile2 = parse(` version: "3.8" services: @@ -182,7 +182,7 @@ networks: `); test("Add suffix to networks in compose file with external and internal networks", () => { - const composeData = load(composeFile2) as ComposeSpecification; + const composeData = parse(composeFile2) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllNetworks(composeData, suffix); @@ -218,7 +218,7 @@ networks: com.docker.network.bridge.enable_icc: "true" `; -const expectedComposeFile3 = load(` +const expectedComposeFile3 = parse(` version: "3.8" services: @@ -247,7 +247,7 @@ networks: `); test("Add suffix to networks in compose file with multiple services and complex network configurations", () => { - const composeData = load(composeFile3) as ComposeSpecification; + const composeData = parse(composeFile3) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllNetworks(composeData, suffix); @@ -289,7 +289,7 @@ networks: `; -const expectedComposeFile4 = load(` +const expectedComposeFile4 = parse(` version: "3.8" services: @@ -326,7 +326,7 @@ networks: `); test("Expect don't add suffix to dokploy-network in compose file with multiple services and complex network configurations", () => { - const composeData = load(composeFile4) as ComposeSpecification; + const composeData = parse(composeFile4) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllNetworks(composeData, suffix); diff --git a/apps/dokploy/__test__/compose/secrets/secret-root.test.ts b/apps/dokploy/__test__/compose/secrets/secret-root.test.ts index b8cef56e4..ef74d64cf 100644 --- a/apps/dokploy/__test__/compose/secrets/secret-root.test.ts +++ b/apps/dokploy/__test__/compose/secrets/secret-root.test.ts @@ -1,7 +1,7 @@ import type { ComposeSpecification } from "@dokploy/server"; import { addSuffixToSecretsRoot, generateRandomHash } from "@dokploy/server"; -import { load } from "js-yaml"; import { expect, test } from "vitest"; +import { parse } from "yaml"; test("Generate random hash with 8 characters", () => { const hash = generateRandomHash(); @@ -23,7 +23,7 @@ secrets: `; test("Add suffix to secrets in root property", () => { - const composeData = load(composeFileSecretsRoot) as ComposeSpecification; + const composeData = parse(composeFileSecretsRoot) as ComposeSpecification; const suffix = generateRandomHash(); if (!composeData?.secrets) { @@ -52,7 +52,7 @@ secrets: `; test("Add suffix to secrets in root property (Test 1)", () => { - const composeData = load(composeFileSecretsRoot1) as ComposeSpecification; + const composeData = parse(composeFileSecretsRoot1) as ComposeSpecification; const suffix = generateRandomHash(); if (!composeData?.secrets) { @@ -84,7 +84,7 @@ secrets: `; test("Add suffix to secrets in root property (Test 2)", () => { - const composeData = load(composeFileSecretsRoot2) as ComposeSpecification; + const composeData = parse(composeFileSecretsRoot2) as ComposeSpecification; const suffix = generateRandomHash(); if (!composeData?.secrets) { diff --git a/apps/dokploy/__test__/compose/secrets/secret-services.test.ts b/apps/dokploy/__test__/compose/secrets/secret-services.test.ts index e12f611d0..a378bd606 100644 --- a/apps/dokploy/__test__/compose/secrets/secret-services.test.ts +++ b/apps/dokploy/__test__/compose/secrets/secret-services.test.ts @@ -3,8 +3,8 @@ import { addSuffixToSecretsInServices, generateRandomHash, } from "@dokploy/server"; -import { load } from "js-yaml"; import { expect, test } from "vitest"; +import { parse } from "yaml"; const composeFileSecretsServices = ` version: "3.8" @@ -21,7 +21,7 @@ secrets: `; test("Add suffix to secrets in services", () => { - const composeData = load(composeFileSecretsServices) as ComposeSpecification; + const composeData = parse(composeFileSecretsServices) as ComposeSpecification; const suffix = generateRandomHash(); if (!composeData.services) { @@ -54,7 +54,9 @@ secrets: `; test("Add suffix to secrets in services (Test 1)", () => { - const composeData = load(composeFileSecretsServices1) as ComposeSpecification; + const composeData = parse( + composeFileSecretsServices1, + ) as ComposeSpecification; const suffix = generateRandomHash(); if (!composeData.services) { @@ -93,7 +95,9 @@ secrets: `; test("Add suffix to secrets in services (Test 2)", () => { - const composeData = load(composeFileSecretsServices2) as ComposeSpecification; + const composeData = parse( + composeFileSecretsServices2, + ) as ComposeSpecification; const suffix = generateRandomHash(); if (!composeData.services) { diff --git a/apps/dokploy/__test__/compose/secrets/secret.test.ts b/apps/dokploy/__test__/compose/secrets/secret.test.ts index 3ff524ad7..3f6544bf1 100644 --- a/apps/dokploy/__test__/compose/secrets/secret.test.ts +++ b/apps/dokploy/__test__/compose/secrets/secret.test.ts @@ -1,7 +1,7 @@ import type { ComposeSpecification } from "@dokploy/server"; import { addSuffixToAllSecrets } from "@dokploy/server"; -import { load } from "js-yaml"; import { expect, test } from "vitest"; +import { parse } from "yaml"; const composeFileCombinedSecrets = ` version: "3.8" @@ -25,7 +25,7 @@ secrets: file: ./app_secret.txt `; -const expectedComposeFileCombinedSecrets = load(` +const expectedComposeFileCombinedSecrets = parse(` version: "3.8" services: @@ -48,7 +48,7 @@ secrets: `) as ComposeSpecification; test("Add suffix to all secrets", () => { - const composeData = load(composeFileCombinedSecrets) as ComposeSpecification; + const composeData = parse(composeFileCombinedSecrets) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllSecrets(composeData, suffix); @@ -77,7 +77,7 @@ secrets: file: ./cache_secret.txt `; -const expectedComposeFileCombinedSecrets3 = load(` +const expectedComposeFileCombinedSecrets3 = parse(` version: "3.8" services: @@ -99,7 +99,9 @@ secrets: `) as ComposeSpecification; test("Add suffix to all secrets (3rd Case)", () => { - const composeData = load(composeFileCombinedSecrets3) as ComposeSpecification; + const composeData = parse( + composeFileCombinedSecrets3, + ) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllSecrets(composeData, suffix); @@ -128,7 +130,7 @@ secrets: file: ./db_password.txt `; -const expectedComposeFileCombinedSecrets4 = load(` +const expectedComposeFileCombinedSecrets4 = parse(` version: "3.8" services: @@ -150,7 +152,9 @@ secrets: `) as ComposeSpecification; test("Add suffix to all secrets (4th Case)", () => { - const composeData = load(composeFileCombinedSecrets4) as ComposeSpecification; + const composeData = parse( + composeFileCombinedSecrets4, + ) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllSecrets(composeData, suffix); diff --git a/apps/dokploy/__test__/compose/service/service-container-name.test.ts b/apps/dokploy/__test__/compose/service/service-container-name.test.ts index 6ad45c588..d6521464d 100644 --- a/apps/dokploy/__test__/compose/service/service-container-name.test.ts +++ b/apps/dokploy/__test__/compose/service/service-container-name.test.ts @@ -1,7 +1,7 @@ import type { ComposeSpecification } from "@dokploy/server"; import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; -import { load } from "js-yaml"; import { expect, test } from "vitest"; +import { parse } from "yaml"; const composeFile = ` version: "3.8" @@ -27,7 +27,7 @@ test("Generate random hash with 8 characters", () => { }); test("Add suffix to service names with container_name in compose file", () => { - const composeData = load(composeFile) as ComposeSpecification; + const composeData = parse(composeFile) as ComposeSpecification; const suffix = generateRandomHash(); diff --git a/apps/dokploy/__test__/compose/service/service-depends-on.test.ts b/apps/dokploy/__test__/compose/service/service-depends-on.test.ts index 14a5789c4..547c309d5 100644 --- a/apps/dokploy/__test__/compose/service/service-depends-on.test.ts +++ b/apps/dokploy/__test__/compose/service/service-depends-on.test.ts @@ -1,7 +1,7 @@ import type { ComposeSpecification } from "@dokploy/server"; import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; -import { load } from "js-yaml"; import { expect, test } from "vitest"; +import { parse } from "yaml"; test("Generate random hash with 8 characters", () => { const hash = generateRandomHash(); @@ -32,7 +32,7 @@ networks: `; test("Add suffix to service names with depends_on (array) in compose file", () => { - const composeData = load(composeFile4) as ComposeSpecification; + const composeData = parse(composeFile4) as ComposeSpecification; const suffix = generateRandomHash(); @@ -102,7 +102,7 @@ networks: `; test("Add suffix to service names with depends_on (object) in compose file", () => { - const composeData = load(composeFile5) as ComposeSpecification; + const composeData = parse(composeFile5) as ComposeSpecification; const suffix = generateRandomHash(); diff --git a/apps/dokploy/__test__/compose/service/service-extends.test.ts b/apps/dokploy/__test__/compose/service/service-extends.test.ts index 0b7e92c53..f539eeebd 100644 --- a/apps/dokploy/__test__/compose/service/service-extends.test.ts +++ b/apps/dokploy/__test__/compose/service/service-extends.test.ts @@ -1,7 +1,7 @@ import type { ComposeSpecification } from "@dokploy/server"; import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; -import { load } from "js-yaml"; import { expect, test } from "vitest"; +import { parse } from "yaml"; test("Generate random hash with 8 characters", () => { const hash = generateRandomHash(); @@ -30,7 +30,7 @@ networks: `; test("Add suffix to service names with extends (string) in compose file", () => { - const composeData = load(composeFile6) as ComposeSpecification; + const composeData = parse(composeFile6) as ComposeSpecification; const suffix = generateRandomHash(); @@ -90,7 +90,7 @@ networks: `; test("Add suffix to service names with extends (object) in compose file", () => { - const composeData = load(composeFile7) as ComposeSpecification; + const composeData = parse(composeFile7) as ComposeSpecification; const suffix = generateRandomHash(); diff --git a/apps/dokploy/__test__/compose/service/service-links.test.ts b/apps/dokploy/__test__/compose/service/service-links.test.ts index 6c8cde39e..4187edce8 100644 --- a/apps/dokploy/__test__/compose/service/service-links.test.ts +++ b/apps/dokploy/__test__/compose/service/service-links.test.ts @@ -1,7 +1,7 @@ import type { ComposeSpecification } from "@dokploy/server"; import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; -import { load } from "js-yaml"; import { expect, test } from "vitest"; +import { parse } from "yaml"; test("Generate random hash with 8 characters", () => { const hash = generateRandomHash(); @@ -31,7 +31,7 @@ networks: `; test("Add suffix to service names with links in compose file", () => { - const composeData = load(composeFile2) as ComposeSpecification; + const composeData = parse(composeFile2) as ComposeSpecification; const suffix = generateRandomHash(); diff --git a/apps/dokploy/__test__/compose/service/service-names.test.ts b/apps/dokploy/__test__/compose/service/service-names.test.ts index c65299b03..c9c9d78c1 100644 --- a/apps/dokploy/__test__/compose/service/service-names.test.ts +++ b/apps/dokploy/__test__/compose/service/service-names.test.ts @@ -1,7 +1,7 @@ import type { ComposeSpecification } from "@dokploy/server"; import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; -import { load } from "js-yaml"; import { expect, test } from "vitest"; +import { parse } from "yaml"; test("Generate random hash with 8 characters", () => { const hash = generateRandomHash(); @@ -26,7 +26,7 @@ networks: `; test("Add suffix to service names in compose file", () => { - const composeData = load(composeFile) as ComposeSpecification; + const composeData = parse(composeFile) as ComposeSpecification; const suffix = generateRandomHash(); diff --git a/apps/dokploy/__test__/compose/service/service.test.ts b/apps/dokploy/__test__/compose/service/service.test.ts index 38895e073..a58e16722 100644 --- a/apps/dokploy/__test__/compose/service/service.test.ts +++ b/apps/dokploy/__test__/compose/service/service.test.ts @@ -3,8 +3,8 @@ import { addSuffixToAllServiceNames, addSuffixToServiceNames, } from "@dokploy/server"; -import { load } from "js-yaml"; import { expect, test } from "vitest"; +import { parse } from "yaml"; const composeFileCombinedAllCases = ` version: "3.8" @@ -38,7 +38,7 @@ networks: driver: bridge `; -const expectedComposeFile = load(` +const expectedComposeFile = parse(` version: "3.8" services: @@ -71,7 +71,9 @@ networks: `); test("Add suffix to all service names in compose file", () => { - const composeData = load(composeFileCombinedAllCases) as ComposeSpecification; + const composeData = parse( + composeFileCombinedAllCases, + ) as ComposeSpecification; const suffix = "testhash"; @@ -131,7 +133,7 @@ networks: driver: bridge `; -const expectedComposeFile1 = load(` +const expectedComposeFile1 = parse(` version: "3.8" services: @@ -176,7 +178,7 @@ networks: `) as ComposeSpecification; test("Add suffix to all service names in compose file 1", () => { - const composeData = load(composeFile1) as ComposeSpecification; + const composeData = parse(composeFile1) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix); @@ -227,7 +229,7 @@ networks: driver: bridge `; -const expectedComposeFile2 = load(` +const expectedComposeFile2 = parse(` version: "3.8" services: @@ -271,7 +273,7 @@ networks: `) as ComposeSpecification; test("Add suffix to all service names in compose file 2", () => { - const composeData = load(composeFile2) as ComposeSpecification; + const composeData = parse(composeFile2) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix); @@ -322,7 +324,7 @@ networks: driver: bridge `; -const expectedComposeFile3 = load(` +const expectedComposeFile3 = parse(` version: "3.8" services: @@ -366,7 +368,7 @@ networks: `) as ComposeSpecification; test("Add suffix to all service names in compose file 3", () => { - const composeData = load(composeFile3) as ComposeSpecification; + const composeData = parse(composeFile3) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix); diff --git a/apps/dokploy/__test__/compose/service/sevice-volumes-from.test.ts b/apps/dokploy/__test__/compose/service/sevice-volumes-from.test.ts index 8aa8296e8..1de94b894 100644 --- a/apps/dokploy/__test__/compose/service/sevice-volumes-from.test.ts +++ b/apps/dokploy/__test__/compose/service/sevice-volumes-from.test.ts @@ -1,7 +1,7 @@ import type { ComposeSpecification } from "@dokploy/server"; import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; -import { load } from "js-yaml"; import { expect, test } from "vitest"; +import { parse } from "yaml"; test("Generate random hash with 8 characters", () => { const hash = generateRandomHash(); @@ -35,7 +35,7 @@ networks: `; test("Add suffix to service names with volumes_from in compose file", () => { - const composeData = load(composeFile3) as ComposeSpecification; + const composeData = parse(composeFile3) as ComposeSpecification; const suffix = generateRandomHash(); diff --git a/apps/dokploy/__test__/compose/volume/volume-2.test.ts b/apps/dokploy/__test__/compose/volume/volume-2.test.ts index 6aa9d01d3..7ffbc4c1a 100644 --- a/apps/dokploy/__test__/compose/volume/volume-2.test.ts +++ b/apps/dokploy/__test__/compose/volume/volume-2.test.ts @@ -4,8 +4,8 @@ import { addSuffixToVolumesRoot, generateRandomHash, } from "@dokploy/server"; -import { load } from "js-yaml"; import { expect, test } from "vitest"; +import { parse } from "yaml"; const composeFile = ` services: @@ -70,7 +70,7 @@ volumes: driver: local `; -const expectedDockerCompose = load(` +const expectedDockerCompose = parse(` services: mail: image: bytemark/smtp @@ -143,7 +143,7 @@ test("Generate random hash with 8 characters", () => { // Docker compose needs unique names for services, volumes, networks and containers // So base on a input which is a dockercompose file, it should replace the name with a hash and return a new dockercompose file test("Add suffix to volumes root property", () => { - const composeData = load(composeFile) as ComposeSpecification; + const composeData = parse(composeFile) as ComposeSpecification; const suffix = generateRandomHash(); @@ -165,7 +165,7 @@ test("Add suffix to volumes root property", () => { }); test("Expect to change the suffix in all the possible places", () => { - const composeData = load(composeFile) as ComposeSpecification; + const composeData = parse(composeFile) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllVolumes(composeData, suffix); @@ -195,7 +195,7 @@ volumes: mongo-data: `; -const expectedDockerCompose2 = load(` +const expectedDockerCompose2 = parse(` version: '3.8' services: app: @@ -218,7 +218,7 @@ volumes: `) as ComposeSpecification; test("Expect to change the suffix in all the possible places (2 Try)", () => { - const composeData = load(composeFile2) as ComposeSpecification; + const composeData = parse(composeFile2) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllVolumes(composeData, suffix); @@ -248,7 +248,7 @@ volumes: mongo-data: `; -const expectedDockerCompose3 = load(` +const expectedDockerCompose3 = parse(` version: '3.8' services: app: @@ -271,7 +271,7 @@ volumes: `) as ComposeSpecification; test("Expect to change the suffix in all the possible places (3 Try)", () => { - const composeData = load(composeFile3) as ComposeSpecification; + const composeData = parse(composeFile3) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllVolumes(composeData, suffix); @@ -645,7 +645,7 @@ volumes: db-config: `; -const expectedDockerComposeComplex = load(` +const expectedDockerComposeComplex = parse(` version: "3.8" services: studio: @@ -1012,7 +1012,7 @@ volumes: `); test("Expect to change the suffix in all the possible places (4 Try)", () => { - const composeData = load(composeFileComplex) as ComposeSpecification; + const composeData = parse(composeFileComplex) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllVolumes(composeData, suffix); @@ -1065,7 +1065,7 @@ volumes: db-data: `; -const expectedDockerComposeExample1 = load(` +const expectedDockerComposeExample1 = parse(` version: "3.8" services: web: @@ -1111,7 +1111,7 @@ volumes: `) as ComposeSpecification; test("Expect to change the suffix in all the possible places (5 Try)", () => { - const composeData = load(composeFileExample1) as ComposeSpecification; + const composeData = parse(composeFileExample1) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllVolumes(composeData, suffix); @@ -1143,7 +1143,7 @@ volumes: backrest-cache: `; -const expectedDockerComposeBackrest = load(` +const expectedDockerComposeBackrest = parse(` services: backrest: image: garethgeorge/backrest:v1.7.3 @@ -1168,7 +1168,7 @@ volumes: `) as ComposeSpecification; test("Should handle volume paths with subdirectories correctly", () => { - const composeData = load(composeFileBackrest) as ComposeSpecification; + const composeData = parse(composeFileBackrest) as ComposeSpecification; const suffix = "testhash"; const updatedComposeData = addSuffixToAllVolumes(composeData, suffix); diff --git a/apps/dokploy/__test__/compose/volume/volume-root.test.ts b/apps/dokploy/__test__/compose/volume/volume-root.test.ts index 80db1f0cc..69afb7f99 100644 --- a/apps/dokploy/__test__/compose/volume/volume-root.test.ts +++ b/apps/dokploy/__test__/compose/volume/volume-root.test.ts @@ -1,7 +1,7 @@ import type { ComposeSpecification } from "@dokploy/server"; import { addSuffixToVolumesRoot, generateRandomHash } from "@dokploy/server"; -import { load } from "js-yaml"; import { expect, test } from "vitest"; +import { parse } from "yaml"; const composeFile = ` version: "3.8" @@ -29,7 +29,7 @@ test("Generate random hash with 8 characters", () => { }); test("Add suffix to volumes in root property", () => { - const composeData = load(composeFile) as ComposeSpecification; + const composeData = parse(composeFile) as ComposeSpecification; const suffix = generateRandomHash(); @@ -67,7 +67,7 @@ networks: `; test("Add suffix to volumes in root property (Case 2)", () => { - const composeData = load(composeFile2) as ComposeSpecification; + const composeData = parse(composeFile2) as ComposeSpecification; const suffix = generateRandomHash(); @@ -101,7 +101,7 @@ networks: `; test("Add suffix to volumes in root property (Case 3)", () => { - const composeData = load(composeFile3) as ComposeSpecification; + const composeData = parse(composeFile3) as ComposeSpecification; const suffix = generateRandomHash(); @@ -148,7 +148,7 @@ volumes: `; // Expected compose file con el prefijo `testhash` -const expectedComposeFile4 = load(` +const expectedComposeFile4 = parse(` version: "3.8" services: @@ -179,7 +179,7 @@ volumes: `) as ComposeSpecification; test("Add suffix to volumes in root property", () => { - const composeData = load(composeFile4) as ComposeSpecification; + const composeData = parse(composeFile4) as ComposeSpecification; const suffix = "testhash"; diff --git a/apps/dokploy/__test__/compose/volume/volume-services.test.ts b/apps/dokploy/__test__/compose/volume/volume-services.test.ts index 0e9cb018f..a42ab5fa9 100644 --- a/apps/dokploy/__test__/compose/volume/volume-services.test.ts +++ b/apps/dokploy/__test__/compose/volume/volume-services.test.ts @@ -3,8 +3,8 @@ import { addSuffixToVolumesInServices, generateRandomHash, } from "@dokploy/server"; -import { load } from "js-yaml"; import { expect, test } from "vitest"; +import { parse } from "yaml"; test("Generate random hash with 8 characters", () => { const hash = generateRandomHash(); @@ -24,7 +24,7 @@ services: `; test("Add suffix to volumes declared directly in services", () => { - const composeData = load(composeFile1) as ComposeSpecification; + const composeData = parse(composeFile1) as ComposeSpecification; const suffix = generateRandomHash(); @@ -59,7 +59,7 @@ volumes: `; test("Add suffix to volumes declared directly in services (Case 2)", () => { - const composeData = load(composeFileTypeVolume) as ComposeSpecification; + const composeData = parse(composeFileTypeVolume) as ComposeSpecification; const suffix = generateRandomHash(); diff --git a/apps/dokploy/__test__/compose/volume/volume.test.ts b/apps/dokploy/__test__/compose/volume/volume.test.ts index 6f8e76708..2ccd12da6 100644 --- a/apps/dokploy/__test__/compose/volume/volume.test.ts +++ b/apps/dokploy/__test__/compose/volume/volume.test.ts @@ -1,7 +1,7 @@ import type { ComposeSpecification } from "@dokploy/server"; import { addSuffixToAllVolumes } from "@dokploy/server"; -import { load } from "js-yaml"; import { expect, test } from "vitest"; +import { parse } from "yaml"; const composeFileTypeVolume = ` version: "3.8" @@ -23,7 +23,7 @@ volumes: driver: local `; -const expectedComposeFileTypeVolume = load(` +const expectedComposeFileTypeVolume = parse(` version: "3.8" services: @@ -44,7 +44,7 @@ volumes: `) as ComposeSpecification; test("Add suffix to volumes with type: volume in services", () => { - const composeData = load(composeFileTypeVolume) as ComposeSpecification; + const composeData = parse(composeFileTypeVolume) as ComposeSpecification; const suffix = "testhash"; @@ -73,7 +73,7 @@ volumes: driver: local `; -const expectedComposeFileTypeVolume1 = load(` +const expectedComposeFileTypeVolume1 = parse(` version: "3.8" services: @@ -93,7 +93,7 @@ volumes: `) as ComposeSpecification; test("Add suffix to mixed volumes in services", () => { - const composeData = load(composeFileTypeVolume1) as ComposeSpecification; + const composeData = parse(composeFileTypeVolume1) as ComposeSpecification; const suffix = "testhash"; @@ -128,7 +128,7 @@ volumes: device: /path/to/app/logs `; -const expectedComposeFileTypeVolume2 = load(` +const expectedComposeFileTypeVolume2 = parse(` version: "3.8" services: @@ -154,7 +154,7 @@ volumes: `) as ComposeSpecification; test("Add suffix to complex volume configurations in services", () => { - const composeData = load(composeFileTypeVolume2) as ComposeSpecification; + const composeData = parse(composeFileTypeVolume2) as ComposeSpecification; const suffix = "testhash"; @@ -218,7 +218,7 @@ volumes: device: /path/to/shared/logs `; -const expectedComposeFileTypeVolume3 = load(` +const expectedComposeFileTypeVolume3 = parse(` version: "3.8" services: @@ -273,7 +273,7 @@ volumes: `) as ComposeSpecification; test("Add suffix to complex nested volumes configuration in services", () => { - const composeData = load(composeFileTypeVolume3) as ComposeSpecification; + const composeData = parse(composeFileTypeVolume3) as ComposeSpecification; const suffix = "testhash"; diff --git a/apps/dokploy/components/dashboard/application/advanced/traefik/update-traefik-config.tsx b/apps/dokploy/components/dashboard/application/advanced/traefik/update-traefik-config.tsx index 73512837f..bf3d5d9bc 100644 --- a/apps/dokploy/components/dashboard/application/advanced/traefik/update-traefik-config.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/traefik/update-traefik-config.tsx @@ -1,8 +1,8 @@ import { zodResolver } from "@hookform/resolvers/zod"; -import jsyaml from "js-yaml"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; +import { parse, stringify, YAMLParseError } from "yaml"; import { z } from "zod"; import { AlertBlock } from "@/components/shared/alert-block"; import { CodeEditor } from "@/components/shared/code-editor"; @@ -38,11 +38,11 @@ interface Props { export const validateAndFormatYAML = (yamlText: string) => { try { - const obj = jsyaml.load(yamlText); - const formattedYaml = jsyaml.dump(obj, { indent: 4 }); + const obj = parse(yamlText); + const formattedYaml = stringify(obj, { indent: 4 }); return { valid: true, formattedYaml, error: null }; } catch (error) { - if (error instanceof jsyaml.YAMLException) { + if (error instanceof YAMLParseError) { return { valid: false, formattedYaml: yamlText, @@ -89,7 +89,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => { if (!valid) { form.setError("traefikConfig", { type: "manual", - message: error || "Invalid YAML", + message: (error as string) || "Invalid YAML", }); return; } diff --git a/apps/dokploy/components/dashboard/project/add-template.tsx b/apps/dokploy/components/dashboard/project/add-template.tsx index 38ff85d15..72c42da49 100644 --- a/apps/dokploy/components/dashboard/project/add-template.tsx +++ b/apps/dokploy/components/dashboard/project/add-template.tsx @@ -171,7 +171,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => { setQuery(e.target.value)} - className="w-full sm:w-[200px]" + className="w-full" value={query} /> { onClick={() => setViewMode(viewMode === "detailed" ? "icon" : "detailed") } - className="h-9 w-9" + className="h-9 w-9 flex-shrink-0" > {viewMode === "detailed" ? ( diff --git a/apps/dokploy/components/dashboard/project/ai/step-three.tsx b/apps/dokploy/components/dashboard/project/ai/step-three.tsx index 188cd5257..bf074f3bd 100644 --- a/apps/dokploy/components/dashboard/project/ai/step-three.tsx +++ b/apps/dokploy/components/dashboard/project/ai/step-three.tsx @@ -88,7 +88,7 @@ export const StepThree = ({ templateInfo }: StepProps) => {

Configuration Files

-
+
+ {isBitbucket && + gitProvider.bitbucket?.appPassword && + !gitProvider.bitbucket?.apiToken ? ( + Deprecated + ) : null} + {!haveGithubRequirements && isGithub && (
{ export const extractCommitedPaths = async ( body: any, - bitbucketUsername: string | null, - bitbucketAppPassword: string | null, - repository: string | null, + bitbucket: Bitbucket | null, + repository: string, ) => { const changes = body.push?.changes || []; @@ -365,18 +369,16 @@ export const extractCommitedPaths = async ( .filter(Boolean); const commitedPaths: string[] = []; for (const commit of commitHashes) { - const url = `https://api.bitbucket.org/2.0/repositories/${bitbucketUsername}/${repository}/diffstat/${commit}`; + const url = `https://api.bitbucket.org/2.0/repositories/${bitbucket?.bitbucketUsername}/${repository}/diffstat/${commit}`; try { const response = await fetch(url, { - headers: { - Authorization: `Basic ${Buffer.from(`${bitbucketUsername}:${bitbucketAppPassword}`).toString("base64")}`, - }, + headers: getBitbucketHeaders(bitbucket!), }); const data = await response.json(); for (const value of data.values) { - commitedPaths.push(value.new?.path); + if (value?.new?.path) commitedPaths.push(value.new.path); } } catch (error) { console.error( diff --git a/apps/dokploy/pages/api/deploy/compose/[refreshToken].ts b/apps/dokploy/pages/api/deploy/compose/[refreshToken].ts index b2232e900..61c7f7157 100644 --- a/apps/dokploy/pages/api/deploy/compose/[refreshToken].ts +++ b/apps/dokploy/pages/api/deploy/compose/[refreshToken].ts @@ -99,8 +99,7 @@ export default async function handler( const commitedPaths = await extractCommitedPaths( req.body, - composeResult.bitbucketOwner, - composeResult.bitbucket?.appPassword || "", + composeResult.bitbucket, composeResult.bitbucketRepository || "", ); diff --git a/apps/dokploy/server/api/routers/cluster.ts b/apps/dokploy/server/api/routers/cluster.ts index 7dde96df5..6c118d802 100644 --- a/apps/dokploy/server/api/routers/cluster.ts +++ b/apps/dokploy/server/api/routers/cluster.ts @@ -7,7 +7,7 @@ import { } from "@dokploy/server"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; -import { getPublicIpWithFallback } from "@/server/wss/terminal"; +import { getLocalServerIp } from "@/server/wss/terminal"; import { createTRPCRouter, protectedProcedure } from "../trpc"; export const clusterRouter = createTRPCRouter({ getNodes: protectedProcedure @@ -61,7 +61,7 @@ export const clusterRouter = createTRPCRouter({ const result = await docker.swarmInspect(); const docker_version = await docker.version(); - let ip = await getPublicIpWithFallback(); + let ip = await getLocalServerIp(); if (input.serverId) { const server = await findServerById(input.serverId); ip = server?.ipAddress; @@ -85,7 +85,7 @@ export const clusterRouter = createTRPCRouter({ const result = await docker.swarmInspect(); const docker_version = await docker.version(); - let ip = await getPublicIpWithFallback(); + let ip = await getLocalServerIp(); if (input.serverId) { const server = await findServerById(input.serverId); ip = server?.ipAddress; diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts index 64f844faf..512ea9718 100644 --- a/apps/dokploy/server/api/routers/compose.ts +++ b/apps/dokploy/server/api/routers/compose.ts @@ -39,10 +39,10 @@ import { import { processTemplate } from "@dokploy/server/templates/processors"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; -import { dump } from "js-yaml"; import _ from "lodash"; import { nanoid } from "nanoid"; import { parse } from "toml"; +import { stringify } from "yaml"; import { z } from "zod"; import { slugify } from "@/lib/slug"; import { db } from "@/server/db"; @@ -364,7 +364,7 @@ export const composeRouter = createTRPCRouter({ } const domains = await findDomainsByComposeId(input.composeId); const composeFile = await addDomainToCompose(compose, domains); - return dump(composeFile, { + return stringify(composeFile, { lineWidth: 1000, }); }), diff --git a/apps/dokploy/server/api/routers/github.ts b/apps/dokploy/server/api/routers/github.ts index 141c309e0..9e739c25c 100644 --- a/apps/dokploy/server/api/routers/github.ts +++ b/apps/dokploy/server/api/routers/github.ts @@ -3,6 +3,7 @@ import { getGithubBranches, getGithubRepositories, haveGithubRequirements, + updateGithub, updateGitProvider, } from "@dokploy/server"; import { TRPCError } from "@trpc/server"; @@ -134,5 +135,9 @@ export const githubRouter = createTRPCRouter({ name: input.name, organizationId: ctx.session.activeOrganizationId, }); + + await updateGithub(input.githubId, { + ...input, + }); }), }); diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index 02678b990..b4968c260 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -46,8 +46,8 @@ import { import { generateOpenApiDocument } from "@dokploy/trpc-openapi"; import { TRPCError } from "@trpc/server"; import { eq, sql } from "drizzle-orm"; -import { dump, load } from "js-yaml"; import { scheduledJobs, scheduleJob } from "node-schedule"; +import { parse, stringify } from "yaml"; import { z } from "zod"; import { db } from "@/server/db"; import { @@ -657,7 +657,7 @@ export const settingsRouter = createTRPCRouter({ const config = readMainConfig(); if (!config) return false; - const parsedConfig = load(config) as { + const parsedConfig = parse(config) as { accessLog?: { filePath: string; }; @@ -678,7 +678,7 @@ export const settingsRouter = createTRPCRouter({ const mainConfig = readMainConfig(); if (!mainConfig) return false; - const currentConfig = load(mainConfig) as { + const currentConfig = parse(mainConfig) as { accessLog?: { filePath: string; }; @@ -701,7 +701,7 @@ export const settingsRouter = createTRPCRouter({ currentConfig.accessLog = undefined; } - writeMainConfig(dump(currentConfig)); + writeMainConfig(stringify(currentConfig)); return true; }), diff --git a/apps/dokploy/server/api/routers/user.ts b/apps/dokploy/server/api/routers/user.ts index 2e7c7a0c5..d30b99b3a 100644 --- a/apps/dokploy/server/api/routers/user.ts +++ b/apps/dokploy/server/api/routers/user.ts @@ -192,7 +192,16 @@ export const userRouter = createTRPCRouter({ }) .where(eq(account.userId, ctx.user.id)); } - return await updateUser(ctx.user.id, input); + + try { + return await updateUser(ctx.user.id, input); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: + error instanceof Error ? error.message : "Failed to update user", + }); + } }), getUserByToken: publicProcedure .input(apiFindOneToken) diff --git a/apps/dokploy/server/wss/terminal.ts b/apps/dokploy/server/wss/terminal.ts index c24be3122..fa37d492b 100644 --- a/apps/dokploy/server/wss/terminal.ts +++ b/apps/dokploy/server/wss/terminal.ts @@ -1,5 +1,10 @@ import type http from "node:http"; -import { findServerById, IS_CLOUD, validateRequest } from "@dokploy/server"; +import { + execAsync, + findServerById, + IS_CLOUD, + validateRequest, +} from "@dokploy/server"; import { publicIpv4, publicIpv6 } from "public-ip"; import { Client, type ConnectConfig } from "ssh2"; import { WebSocketServer } from "ws"; @@ -44,6 +49,21 @@ export const getPublicIpWithFallback = async () => { return ip; }; +export const getLocalServerIp = async () => { + try { + const command = `ip addr show | grep -E "inet (192\.168\.|10\.|172\.1[6-9]\.|172\.2[0-9]\.|172\.3[0-1]\.)" | head -n1 | awk '{print $2}' | cut -d/ -f1`; + const { stdout } = await execAsync(command); + const ip = stdout.trim(); + return ( + ip || + "We were unable to obtain the local server IP, please use your private IP address" + ); + } catch (error) { + console.error("Error to obtain local server IP", error); + return "We were unable to obtain the local server IP, please use your private IP address"; + } +}; + export const setupTerminalWebSocketServer = ( server: http.Server, ) => { diff --git a/apps/dokploy/setup.ts b/apps/dokploy/setup.ts index 13590e4e7..55e1da87c 100644 --- a/apps/dokploy/setup.ts +++ b/apps/dokploy/setup.ts @@ -1,3 +1,4 @@ +import { exit } from "node:process"; import { execAsync } from "@dokploy/server"; import { setupDirectories } from "@dokploy/server/setup/config-paths"; import { initializePostgres } from "@dokploy/server/setup/postgres-setup"; @@ -25,6 +26,8 @@ import { await initializeStandaloneTraefik(); await initializeRedis(); await initializePostgres(); + console.log("Dokploy setup completed"); + exit(0); } catch (e) { console.error("Error in dokploy setup", e); } diff --git a/packages/server/package.json b/packages/server/package.json index 3b249a65b..4d0f2e804 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -57,7 +57,7 @@ "drizzle-orm": "^0.39.3", "drizzle-zod": "0.5.1", "hi-base32": "^0.5.1", - "js-yaml": "4.1.0", + "yaml": "2.8.1", "lodash": "4.17.21", "micromatch": "4.0.8", "nanoid": "3.3.11", @@ -85,7 +85,6 @@ "@types/adm-zip": "^0.5.7", "@types/bcrypt": "5.0.2", "@types/dockerode": "3.3.23", - "@types/js-yaml": "4.0.9", "@types/lodash": "4.17.4", "@types/micromatch": "4.0.9", "@types/node": "^18.19.104", diff --git a/packages/server/src/db/schema/bitbucket.ts b/packages/server/src/db/schema/bitbucket.ts index 0311202d7..cc7cbb8ea 100644 --- a/packages/server/src/db/schema/bitbucket.ts +++ b/packages/server/src/db/schema/bitbucket.ts @@ -11,7 +11,9 @@ export const bitbucket = pgTable("bitbucket", { .primaryKey() .$defaultFn(() => nanoid()), bitbucketUsername: text("bitbucketUsername"), + bitbucketEmail: text("bitbucketEmail"), appPassword: text("appPassword"), + apiToken: text("apiToken"), bitbucketWorkspaceName: text("bitbucketWorkspaceName"), gitProviderId: text("gitProviderId") .notNull() @@ -29,7 +31,9 @@ const createSchema = createInsertSchema(bitbucket); export const apiCreateBitbucket = createSchema.extend({ bitbucketUsername: z.string().optional(), + bitbucketEmail: z.string().email().optional(), appPassword: z.string().optional(), + apiToken: z.string().optional(), bitbucketWorkspaceName: z.string().optional(), gitProviderId: z.string().optional(), authId: z.string().min(1), @@ -46,9 +50,19 @@ export const apiBitbucketTestConnection = createSchema .extend({ bitbucketId: z.string().min(1), bitbucketUsername: z.string().optional(), + bitbucketEmail: z.string().email().optional(), workspaceName: z.string().optional(), + apiToken: z.string().optional(), + appPassword: z.string().optional(), }) - .pick({ bitbucketId: true, bitbucketUsername: true, workspaceName: true }); + .pick({ + bitbucketId: true, + bitbucketUsername: true, + bitbucketEmail: true, + workspaceName: true, + apiToken: true, + appPassword: true, + }); export const apiFindBitbucketBranches = z.object({ owner: z.string(), @@ -60,6 +74,9 @@ export const apiUpdateBitbucket = createSchema.extend({ bitbucketId: z.string().min(1), name: z.string().min(1), bitbucketUsername: z.string().optional(), + bitbucketEmail: z.string().email().optional(), + appPassword: z.string().optional(), + apiToken: z.string().optional(), bitbucketWorkspaceName: z.string().optional(), organizationId: z.string().optional(), }); diff --git a/packages/server/src/db/schema/github.ts b/packages/server/src/db/schema/github.ts index b8f739ceb..fb8a267a7 100644 --- a/packages/server/src/db/schema/github.ts +++ b/packages/server/src/db/schema/github.ts @@ -58,4 +58,5 @@ export const apiUpdateGithub = createSchema.extend({ githubId: z.string().min(1), name: z.string().min(1), gitProviderId: z.string().min(1), + githubAppName: z.string().min(1), }); diff --git a/packages/server/src/db/schema/user.ts b/packages/server/src/db/schema/user.ts index 933a7490c..ca92f50e8 100644 --- a/packages/server/src/db/schema/user.ts +++ b/packages/server/src/db/schema/user.ts @@ -322,6 +322,11 @@ export const apiUpdateWebServerMonitoring = z.object({ }); export const apiUpdateUser = createSchema.partial().extend({ + email: z + .string() + .email("Please enter a valid email address") + .min(1, "Email is required") + .optional(), password: z.string().optional(), currentPassword: z.string().optional(), name: z.string().optional(), diff --git a/packages/server/src/services/ai.ts b/packages/server/src/services/ai.ts index 61c448397..7056f1547 100644 --- a/packages/server/src/services/ai.ts +++ b/packages/server/src/services/ai.ts @@ -92,31 +92,48 @@ export const suggestVariants = async ({ const { object } = await generateObject({ model, - output: "array", + output: "object", schema: z.object({ - id: z.string(), - name: z.string(), - shortDescription: z.string(), - description: z.string(), + suggestions: z.array( + z.object({ + id: z.string(), + name: z.string(), + shortDescription: z.string(), + description: z.string(), + }), + ), }), prompt: ` - Act as advanced DevOps engineer and generate a list of open source projects what can cover users needs(up to 3 items), the suggestion - should include id, name, shortDescription, and description. Use slug of title for id. + Act as advanced DevOps engineer and generate a list of open source projects what can cover users needs(up to 3 items). + + Return your response as a JSON object with the following structure: + { + "suggestions": [ + { + "id": "project-slug", + "name": "Project Name", + "shortDescription": "Brief one-line description", + "description": "Detailed description" + } + ] + } Important rules for the response: - 1. The description field should ONLY contain a plain text description of the project, its features, and use cases - 2. Do NOT include any code snippets, configuration examples, or installation instructions in the description - 3. The shortDescription should be a single-line summary focusing on the main technologies + 1. Use slug format for the id field (lowercase, hyphenated) + 2. The description field should ONLY contain a plain text description of the project, its features, and use cases + 3. Do NOT include any code snippets, configuration examples, or installation instructions in the description + 4. The shortDescription should be a single-line summary focusing on the main technologies + 5. All projects should be installable in docker and have docker compose support - User wants to create a new project with the following details, it should be installable in docker and can be docker compose generated for it: + User wants to create a new project with the following details: ${input} `, }); - if (object?.length) { + if (object?.suggestions?.length) { const result = []; - for (const suggestion of object) { + for (const suggestion of object.suggestions) { try { const { object: docker } = await generateObject({ model, @@ -136,16 +153,29 @@ export const suggestVariants = async ({ serviceName: z.string(), }), ), - configFiles: z.array( - z.object({ - content: z.string(), - filePath: z.string(), - }), - ), + configFiles: z + .array( + z.object({ + content: z.string(), + filePath: z.string(), + }), + ) + .optional(), }), prompt: ` Act as advanced DevOps engineer and generate docker compose with environment variables and domain configurations needed to install the following project. - Return the docker compose as a YAML string and environment variables configuration. Follow these rules: + + Return your response as a JSON object with this structure: + { + "dockerCompose": "yaml string here", + "envVariables": [{"name": "VAR_NAME", "value": "example_value"}], + "domains": [{"host": "domain.com", "port": 3000, "serviceName": "service"}], + "configFiles": [{"content": "file content", "filePath": "path/to/file"}] + } + + Note: configFiles is optional - only include it if configuration files are absolutely required. + + Follow these rules: Docker Compose Rules: 1. Use placeholder like \${VARIABLE_NAME-default} for generated variables in the docker-compose.yml @@ -198,6 +228,7 @@ export const suggestVariants = async ({ console.error("Error in docker compose generation:", error); } } + return result; } diff --git a/packages/server/src/services/bitbucket.ts b/packages/server/src/services/bitbucket.ts index 387dbdf14..4cd48f957 100644 --- a/packages/server/src/services/bitbucket.ts +++ b/packages/server/src/services/bitbucket.ts @@ -68,10 +68,26 @@ export const updateBitbucket = async ( input: typeof apiUpdateBitbucket._type, ) => { return await db.transaction(async (tx) => { + // First get the current bitbucket provider to get gitProviderId + const currentProvider = await tx.query.bitbucket.findFirst({ + where: eq(bitbucket.bitbucketId, bitbucketId), + }); + + if (!currentProvider) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Bitbucket provider not found", + }); + } + const result = await tx .update(bitbucket) .set({ - ...input, + bitbucketUsername: input.bitbucketUsername, + bitbucketEmail: input.bitbucketEmail, + appPassword: input.appPassword, + apiToken: input.apiToken, + bitbucketWorkspaceName: input.bitbucketWorkspaceName, }) .where(eq(bitbucket.bitbucketId, bitbucketId)) .returning(); @@ -83,7 +99,7 @@ export const updateBitbucket = async ( name: input.name, organizationId: input.organizationId, }) - .where(eq(gitProvider.gitProviderId, input.gitProviderId)) + .where(eq(gitProvider.gitProviderId, currentProvider.gitProviderId)) .returning(); } diff --git a/packages/server/src/services/certificate.ts b/packages/server/src/services/certificate.ts index f59f1c2aa..8707d098a 100644 --- a/packages/server/src/services/certificate.ts +++ b/packages/server/src/services/certificate.ts @@ -9,7 +9,7 @@ import { import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; -import { dump } from "js-yaml"; +import { stringify } from "yaml"; import type { z } from "zod"; import { encodeBase64 } from "../utils/docker/utils"; import { execAsyncRemote } from "../utils/process/execAsync"; @@ -101,7 +101,7 @@ const createCertificateFiles = async (certificate: Certificate) => { ], }, }; - const yamlConfig = dump(traefikConfig); + const yamlConfig = stringify(traefikConfig); const configFile = path.join(certDir, "certificate.yml"); if (certificate.serverId) { diff --git a/packages/server/src/services/registry.ts b/packages/server/src/services/registry.ts index 6468cd970..ec8db8fa8 100644 --- a/packages/server/src/services/registry.ts +++ b/packages/server/src/services/registry.ts @@ -10,6 +10,22 @@ import { IS_CLOUD } from "../constants"; export type Registry = typeof registry.$inferSelect; +function shEscape(s: string | undefined): string { + if (!s) return "''"; + return `'${s.replace(/'/g, `'\\''`)}'`; +} + +function safeDockerLoginCommand( + registry: string | undefined, + user: string | undefined, + pass: string | undefined, +) { + const escapedRegistry = shEscape(registry); + const escapedUser = shEscape(user); + const escapedPassword = shEscape(pass); + return `printf %s ${escapedPassword} | docker login ${escapedRegistry} -u ${escapedUser} --password-stdin`; +} + export const createRegistry = async ( input: typeof apiCreateRegistry._type, organizationId: string, @@ -37,7 +53,11 @@ export const createRegistry = async ( message: "Select a server to add the registry", }); } - const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`; + const loginCommand = safeDockerLoginCommand( + input.registryUrl, + input.username, + input.password, + ); if (input.serverId && input.serverId !== "none") { await execAsyncRemote(input.serverId, loginCommand); } else if (newRegistry.registryType === "cloud") { @@ -91,7 +111,11 @@ export const updateRegistry = async ( .returning() .then((res) => res[0]); - const loginCommand = `echo ${response?.password} | docker login ${response?.registryUrl} --username ${response?.username} --password-stdin`; + const loginCommand = safeDockerLoginCommand( + response?.registryUrl, + response?.username, + response?.password, + ); if ( IS_CLOUD && diff --git a/packages/server/src/services/user.ts b/packages/server/src/services/user.ts index 728d5b8ee..ae03432a1 100644 --- a/packages/server/src/services/user.ts +++ b/packages/server/src/services/user.ts @@ -296,6 +296,19 @@ export const findMemberById = async ( }; export const updateUser = async (userId: string, userData: Partial) => { + // Validate email if it's being updated + if (userData.email !== undefined) { + if (!userData.email || userData.email.trim() === "") { + throw new Error("Email is required and cannot be empty"); + } + + // Basic email format validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(userData.email)) { + throw new Error("Please enter a valid email address"); + } + } + const user = await db .update(users_temp) .set({ diff --git a/packages/server/src/setup/traefik-setup.ts b/packages/server/src/setup/traefik-setup.ts index 6b98457b9..fa9bf78d0 100644 --- a/packages/server/src/setup/traefik-setup.ts +++ b/packages/server/src/setup/traefik-setup.ts @@ -1,7 +1,14 @@ -import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs"; +import { + chmodSync, + existsSync, + mkdirSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; import path from "node:path"; import type { ContainerCreateOptions, CreateServiceOptions } from "dockerode"; -import { dump } from "js-yaml"; +import { stringify } from "yaml"; import { paths } from "../constants"; import { getRemoteDocker } from "../utils/servers/remote-docker"; import type { FileConfig } from "../utils/traefik/file-types"; @@ -234,7 +241,7 @@ export const createDefaultServerTraefikConfig = () => { }, }; - const yamlStr = dump(config); + const yamlStr = stringify(config); mkdirSync(DYNAMIC_TRAEFIK_PATH, { recursive: true }); writeFileSync( path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`), @@ -308,7 +315,7 @@ export const getDefaultTraefikConfig = () => { }), }; - const yamlStr = dump(configObject); + const yamlStr = stringify(configObject); return yamlStr; }; @@ -362,7 +369,7 @@ export const getDefaultServerTraefikConfig = () => { }, }; - const yamlStr = dump(configObject); + const yamlStr = stringify(configObject); return yamlStr; }; @@ -375,13 +382,26 @@ export const createDefaultTraefikConfig = () => { if (existsSync(acmeJsonPath)) { chmodSync(acmeJsonPath, "600"); } - if (existsSync(mainConfig)) { - console.log("Main config already exists"); - return; - } - const yamlStr = getDefaultTraefikConfig(); + + // Create the traefik directory first mkdirSync(MAIN_TRAEFIK_PATH, { recursive: true }); + + // Check if traefik.yml exists and handle the case where it might be a directory + if (existsSync(mainConfig)) { + const stats = statSync(mainConfig); + if (stats.isDirectory()) { + // If traefik.yml is a directory, remove it + console.log("Found traefik.yml as directory, removing it..."); + rmSync(mainConfig, { recursive: true, force: true }); + } else if (stats.isFile()) { + console.log("Main config already exists"); + return; + } + } + + const yamlStr = getDefaultTraefikConfig(); writeFileSync(mainConfig, yamlStr, "utf8"); + console.log("Traefik config created successfully"); }; export const getDefaultMiddlewares = () => { @@ -397,7 +417,7 @@ export const getDefaultMiddlewares = () => { }, }, }; - const yamlStr = dump(defaultMiddlewares); + const yamlStr = stringify(defaultMiddlewares); return yamlStr; }; export const createDefaultMiddlewares = () => { diff --git a/packages/server/src/utils/backups/utils.ts b/packages/server/src/utils/backups/utils.ts index b4563287b..d9d5150b9 100644 --- a/packages/server/src/utils/backups/utils.ts +++ b/packages/server/src/utils/backups/utils.ts @@ -89,7 +89,7 @@ export const getMariadbBackupCommand = ( databaseUser: string, databasePassword: string, ) => { - return `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; mariadb-dump --user='${databaseUser}' --password='${databasePassword}' --databases ${database} | gzip"`; + return `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; mariadb-dump --user='${databaseUser}' --password='${databasePassword}' --single-transaction --quick --databases ${database} | gzip"`; }; export const getMysqlBackupCommand = ( diff --git a/packages/server/src/utils/builders/index.ts b/packages/server/src/utils/builders/index.ts index 492f799a8..5d3261dbb 100644 --- a/packages/server/src/utils/builders/index.ts +++ b/packages/server/src/utils/builders/index.ts @@ -220,8 +220,8 @@ const getImageName = (application: ApplicationNested) => { if (registry) { const { registryUrl, imagePrefix, username } = registry; const registryTag = imagePrefix - ? `${registryUrl}/${imagePrefix}/${imageName}` - : `${registryUrl}/${username}/${imageName}`; + ? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}` + : `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`; return registryTag; } diff --git a/packages/server/src/utils/cluster/upload.ts b/packages/server/src/utils/cluster/upload.ts index f982d414b..c13a2701c 100644 --- a/packages/server/src/utils/cluster/upload.ts +++ b/packages/server/src/utils/cluster/upload.ts @@ -22,8 +22,8 @@ export const uploadImage = async ( // For ghcr.io: ghcr.io/username/image:tag // For docker.io: docker.io/username/image:tag const registryTag = imagePrefix - ? `${registryUrl}/${imagePrefix}/${imageName}` - : `${registryUrl}/${username}/${imageName}`; + ? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}` + : `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`; try { writeStream.write( diff --git a/packages/server/src/utils/docker/collision.ts b/packages/server/src/utils/docker/collision.ts index de6d9bbb3..9752100ca 100644 --- a/packages/server/src/utils/docker/collision.ts +++ b/packages/server/src/utils/docker/collision.ts @@ -1,5 +1,5 @@ import { findComposeById } from "@dokploy/server/services/compose"; -import { dump } from "js-yaml"; +import { stringify } from "yaml"; import { addAppNameToAllServiceNames } from "./collision/root-network"; import { generateRandomHash } from "./compose"; import { addSuffixToAllVolumes } from "./compose/volume"; @@ -59,7 +59,7 @@ export const randomizeIsolatedDeploymentComposeFile = async ( ) : composeData; - return dump(newComposeFile); + return stringify(newComposeFile); }; export const randomizeDeployableSpecificationFile = ( diff --git a/packages/server/src/utils/docker/compose.ts b/packages/server/src/utils/docker/compose.ts index 2cb909abb..a78b416ec 100644 --- a/packages/server/src/utils/docker/compose.ts +++ b/packages/server/src/utils/docker/compose.ts @@ -1,6 +1,6 @@ import crypto from "node:crypto"; import { findComposeById } from "@dokploy/server/services/compose"; -import { dump, load } from "js-yaml"; +import { parse, stringify } from "yaml"; import { addSuffixToAllConfigs } from "./compose/configs"; import { addSuffixToAllNetworks } from "./compose/network"; import { addSuffixToAllSecrets } from "./compose/secrets"; @@ -18,13 +18,13 @@ export const randomizeComposeFile = async ( ) => { const compose = await findComposeById(composeId); const composeFile = compose.composeFile; - const composeData = load(composeFile) as ComposeSpecification; + const composeData = parse(composeFile) as ComposeSpecification; const randomSuffix = suffix || generateRandomHash(); const newComposeFile = addSuffixToAllProperties(composeData, randomSuffix); - return dump(newComposeFile); + return stringify(newComposeFile); }; export const randomizeSpecificationFile = ( diff --git a/packages/server/src/utils/docker/domain.ts b/packages/server/src/utils/docker/domain.ts index cfb74e974..7a9521d1d 100644 --- a/packages/server/src/utils/docker/domain.ts +++ b/packages/server/src/utils/docker/domain.ts @@ -4,7 +4,7 @@ import { join } from "node:path"; import { paths } from "@dokploy/server/constants"; import type { Compose } from "@dokploy/server/services/compose"; import type { Domain } from "@dokploy/server/services/domain"; -import { dump, load } from "js-yaml"; +import { parse, stringify } from "yaml"; import { execAsyncRemote } from "../process/execAsync"; import { cloneRawBitbucketRepository, @@ -92,7 +92,7 @@ export const loadDockerCompose = async ( if (existsSync(path)) { const yamlStr = readFileSync(path, "utf8"); - const parsedConfig = load(yamlStr) as ComposeSpecification; + const parsedConfig = parse(yamlStr) as ComposeSpecification; return parsedConfig; } return null; @@ -115,7 +115,7 @@ export const loadDockerComposeRemote = async ( return null; } if (!stdout) return null; - const parsedConfig = load(stdout) as ComposeSpecification; + const parsedConfig = parse(stdout) as ComposeSpecification; return parsedConfig; } catch { return null; @@ -141,7 +141,7 @@ export const writeDomainsToCompose = async ( const composeConverted = await addDomainToCompose(compose, domains); const path = getComposePath(compose); - const composeString = dump(composeConverted, { lineWidth: 1000 }); + const composeString = stringify(composeConverted, { lineWidth: 1000 }); try { await writeFile(path, composeString, "utf8"); } catch (error) { @@ -169,7 +169,7 @@ exit 1; `; } if (compose.serverId) { - const composeString = dump(composeConverted, { lineWidth: 1000 }); + const composeString = stringify(composeConverted, { lineWidth: 1000 }); const encodedContent = encodeBase64(composeString); return `echo "${encodedContent}" | base64 -d > "${path}";`; } @@ -287,7 +287,7 @@ export const writeComposeFile = async ( const path = getComposePath(compose); try { - const composeFile = dump(composeSpec, { + const composeFile = stringify(composeSpec, { lineWidth: 1000, }); fs.writeFileSync(path, composeFile, "utf8"); diff --git a/packages/server/src/utils/providers/bitbucket.ts b/packages/server/src/utils/providers/bitbucket.ts index 8f2752a2e..ca76a67fa 100644 --- a/packages/server/src/utils/providers/bitbucket.ts +++ b/packages/server/src/utils/providers/bitbucket.ts @@ -5,7 +5,10 @@ import type { apiBitbucketTestConnection, apiFindBitbucketBranches, } from "@dokploy/server/db/schema"; -import { findBitbucketById } from "@dokploy/server/services/bitbucket"; +import { + type Bitbucket, + findBitbucketById, +} from "@dokploy/server/services/bitbucket"; import type { Compose } from "@dokploy/server/services/compose"; import type { InferResultType } from "@dokploy/server/types/with"; import { TRPCError } from "@trpc/server"; @@ -23,6 +26,39 @@ export type ComposeWithBitbucket = InferResultType< { bitbucket: true } >; +export const getBitbucketCloneUrl = ( + bitbucketProvider: { + apiToken?: string | null; + bitbucketUsername?: string | null; + appPassword?: string | null; + } | null, + repoClone: string, +) => { + if (!bitbucketProvider) { + throw new Error("Bitbucket provider is required"); + } + return bitbucketProvider.apiToken + ? `https://x-token-auth:${bitbucketProvider.apiToken}@${repoClone}` + : `https://${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}@${repoClone}`; +}; + +export const getBitbucketHeaders = (bitbucketProvider: Bitbucket) => { + if (bitbucketProvider.apiToken) { + // For API tokens, use HTTP Basic auth with email and token + // According to Bitbucket docs: email:token for API calls + const email = + bitbucketProvider.bitbucketEmail || bitbucketProvider.bitbucketUsername; + return { + Authorization: `Basic ${Buffer.from(`${email}:${bitbucketProvider.apiToken}`).toString("base64")}`, + }; + } + + // For app passwords, use HTTP Basic auth with username and app password + return { + Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`, + }; +}; + export const cloneBitbucketRepository = async ( entity: ApplicationWithBitbucket | ComposeWithBitbucket, logPath: string, @@ -51,7 +87,7 @@ export const cloneBitbucketRepository = async ( const outputPath = join(basePath, appName, "code"); await recreateDirectory(outputPath); const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`; - const cloneUrl = `https://${bitbucket?.bitbucketUsername}:${bitbucket?.appPassword}@${repoclone}`; + const cloneUrl = getBitbucketCloneUrl(bitbucket, repoclone); try { writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`); const cloneArgs = [ @@ -103,7 +139,7 @@ export const cloneRawBitbucketRepository = async (entity: Compose) => { const outputPath = join(basePath, appName, "code"); await recreateDirectory(outputPath); const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`; - const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`; + const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone); try { const cloneArgs = [ @@ -153,7 +189,7 @@ export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => { const basePath = COMPOSE_PATH; const outputPath = join(basePath, appName, "code"); const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`; - const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`; + const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone); try { const cloneCommand = ` @@ -206,7 +242,7 @@ export const getBitbucketCloneCommand = async ( const outputPath = join(basePath, appName, "code"); await recreateDirectory(outputPath); const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`; - const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`; + const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone); const cloneCommand = ` rm -rf ${outputPath}; @@ -241,9 +277,7 @@ export const getBitbucketRepositories = async (bitbucketId?: string) => { while (url) { const response = await fetch(url, { method: "GET", - headers: { - Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`, - }, + headers: getBitbucketHeaders(bitbucketProvider), }); if (!response.ok) { @@ -279,35 +313,43 @@ export const getBitbucketBranches = async ( } const bitbucketProvider = await findBitbucketById(input.bitbucketId); const { owner, repo } = input; - const url = `https://api.bitbucket.org/2.0/repositories/${owner}/${repo}/refs/branches?pagelen=100`; + let url = `https://api.bitbucket.org/2.0/repositories/${owner}/${repo}/refs/branches?pagelen=1`; + let allBranches: { + name: string; + commit: { + sha: string; + }; + }[] = []; try { - const response = await fetch(url, { - method: "GET", - headers: { - Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`, - }, - }); - - if (!response.ok) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: `HTTP error! status: ${response.status}`, + while (url) { + const response = await fetch(url, { + method: "GET", + headers: getBitbucketHeaders(bitbucketProvider), }); + + if (!response.ok) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `HTTP error! status: ${response.status}`, + }); + } + + const data = await response.json(); + + const mappedData = data.values.map((branch: any) => { + return { + name: branch.name, + commit: { + sha: branch.target.hash, + }, + }; + }); + allBranches = allBranches.concat(mappedData); + url = data.next || null; } - const data = await response.json(); - - const mappedData = data.values.map((branch: any) => { - return { - name: branch.name, - commit: { - sha: branch.target.hash, - }, - }; - }); - - return mappedData as { + return allBranches as { name: string; commit: { sha: string; @@ -335,9 +377,7 @@ export const testBitbucketConnection = async ( try { const response = await fetch(url, { method: "GET", - headers: { - Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`, - }, + headers: getBitbucketHeaders(bitbucketProvider), }); if (!response.ok) { diff --git a/packages/server/src/utils/providers/gitea.ts b/packages/server/src/utils/providers/gitea.ts index 21265bf37..874e2679e 100644 --- a/packages/server/src/utils/providers/gitea.ts +++ b/packages/server/src/utils/providers/gitea.ts @@ -362,17 +362,22 @@ export const testGiteaConnection = async (input: { giteaId: string }) => { } const baseUrl = provider.giteaUrl.replace(/\/+$/, ""); - const limit = 30; - let allRepos = 0; - let nextUrl = `${baseUrl}/api/v1/repos/search?limit=${limit}`; - while (nextUrl) { - const response = await fetch(nextUrl, { - headers: { - Accept: "application/json", - Authorization: `token ${provider.accessToken}`, + // Use /user/repos to get authenticated user's repositories with pagination + let allRepos = 0; + let page = 1; + const limit = 50; // Max per page + + while (true) { + const response = await fetch( + `${baseUrl}/api/v1/user/repos?page=${page}&limit=${limit}`, + { + headers: { + Accept: "application/json", + Authorization: `token ${provider.accessToken}`, + }, }, - }); + ); if (!response.ok) { throw new Error( @@ -381,22 +386,18 @@ export const testGiteaConnection = async (input: { giteaId: string }) => { } const repos = await response.json(); - allRepos += repos.data.length; - - const linkHeader = response.headers.get("link"); - nextUrl = ""; - - if (linkHeader) { - const nextLink = linkHeader - .split(",") - .find((link) => link.includes('rel="next"')); - if (nextLink) { - const matches = nextLink.match(/<([^>]+)>/); - if (matches?.[1]) { - nextUrl = matches[1]; - } - } + if (!Array.isArray(repos) || repos.length === 0) { + break; // No more repositories } + + allRepos += repos.length; + + // Check if there are more pages + if (repos.length < limit) { + break; // Last page (fewer results than limit) + } + + page++; } await updateGitea(giteaId, { @@ -418,17 +419,22 @@ export const getGiteaRepositories = async (giteaId?: string) => { const giteaProvider = await findGiteaById(giteaId); const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, ""); - const limit = 30; - let allRepositories: any[] = []; - let nextUrl = `${baseUrl}/api/v1/repos/search?limit=${limit}`; - while (nextUrl) { - const response = await fetch(nextUrl, { - headers: { - Accept: "application/json", - Authorization: `token ${giteaProvider.accessToken}`, + // Use /user/repos to get authenticated user's repositories with pagination + let allRepositories: any[] = []; + let page = 1; + const limit = 50; // Max per page + + while (true) { + const response = await fetch( + `${baseUrl}/api/v1/user/repos?page=${page}&limit=${limit}`, + { + headers: { + Accept: "application/json", + Authorization: `token ${giteaProvider.accessToken}`, + }, }, - }); + ); if (!response.ok) { throw new TRPCError({ @@ -437,23 +443,19 @@ export const getGiteaRepositories = async (giteaId?: string) => { }); } - const result = await response.json(); - allRepositories = [...allRepositories, ...result.data]; - - const linkHeader = response.headers.get("link"); - nextUrl = ""; - - if (linkHeader) { - const nextLink = linkHeader - .split(",") - .find((link) => link.includes('rel="next"')); - if (nextLink) { - const matches = nextLink.match(/<([^>]+)>/); - if (matches?.[1]) { - nextUrl = matches[1]; - } - } + const repos = await response.json(); + if (!Array.isArray(repos) || repos.length === 0) { + break; // No more repositories } + + allRepositories = [...allRepositories, ...repos]; + + // Check if there are more pages + if (repos.length < limit) { + break; // Last page (fewer results than limit) + } + + page++; } return ( @@ -482,25 +484,43 @@ export const getGiteaBranches = async (input: { const giteaProvider = await findGiteaById(input.giteaId); const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, ""); - const url = `${baseUrl}/api/v1/repos/${input.owner}/${input.repo}/branches`; - const response = await fetch(url, { - headers: { - Accept: "application/json", - Authorization: `token ${giteaProvider.accessToken}`, - }, - }); + // Handle pagination for branches + let allBranches: any[] = []; + let page = 1; + const limit = 50; // Max per page - if (!response.ok) { - throw new Error(`Failed to fetch branches: ${response.statusText}`); + while (true) { + const response = await fetch( + `${baseUrl}/api/v1/repos/${input.owner}/${input.repo}/branches?page=${page}&limit=${limit}`, + { + headers: { + Accept: "application/json", + Authorization: `token ${giteaProvider.accessToken}`, + }, + }, + ); + + if (!response.ok) { + throw new Error(`Failed to fetch branches: ${response.statusText}`); + } + + const branches = await response.json(); + if (!Array.isArray(branches) || branches.length === 0) { + break; // No more branches + } + + allBranches = [...allBranches, ...branches]; + + // Check if there are more pages + if (branches.length < limit) { + break; // Last page (fewer results than limit) + } + + page++; } - const branches = await response.json(); - - if (!branches) { - return []; - } - return branches?.map((branch: any) => ({ + return allBranches?.map((branch: any) => ({ id: branch.name, name: branch.name, commit: { diff --git a/packages/server/src/utils/providers/gitlab.ts b/packages/server/src/utils/providers/gitlab.ts index 4b6bb7965..840347fdb 100644 --- a/packages/server/src/utils/providers/gitlab.ts +++ b/packages/server/src/utils/providers/gitlab.ts @@ -401,7 +401,7 @@ export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => { const { appName, gitlabPathNamespace, - branch, + gitlabBranch, gitlabId, serverId, enableSubmodules, @@ -429,7 +429,7 @@ export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => { try { const command = ` rm -rf ${outputPath}; - git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} + git clone --branch ${gitlabBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} `; await execAsyncRemote(serverId, command); } catch (error) { diff --git a/packages/server/src/utils/traefik/application.ts b/packages/server/src/utils/traefik/application.ts index cd2c30a1f..b18161c76 100644 --- a/packages/server/src/utils/traefik/application.ts +++ b/packages/server/src/utils/traefik/application.ts @@ -3,7 +3,7 @@ import path from "node:path"; import { createInterface } from "node:readline"; import { paths } from "@dokploy/server/constants"; import type { Domain } from "@dokploy/server/services/domain"; -import { dump, load } from "js-yaml"; +import { parse, stringify } from "yaml"; import { encodeBase64 } from "../docker/utils"; import { execAsyncRemote } from "../process/execAsync"; import type { FileConfig, HttpLoadBalancerService } from "./file-types"; @@ -40,7 +40,7 @@ export const createTraefikConfig = (appName: string) => { }, }, }; - const yamlStr = dump(config); + const yamlStr = stringify(config); const { DYNAMIC_TRAEFIK_PATH } = paths(); fs.mkdirSync(DYNAMIC_TRAEFIK_PATH, { recursive: true }); writeFileSync( @@ -87,7 +87,7 @@ export const loadOrCreateConfig = (appName: string): FileConfig => { const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`); if (fs.existsSync(configPath)) { const yamlStr = fs.readFileSync(configPath, "utf8"); - const parsedConfig = (load(yamlStr) as FileConfig) || { + const parsedConfig = (parse(yamlStr) as FileConfig) || { http: { routers: {}, services: {} }, }; return parsedConfig; @@ -107,7 +107,7 @@ export const loadOrCreateConfigRemote = async ( if (!stdout) return fileConfig; - const parsedConfig = (load(stdout) as FileConfig) || { + const parsedConfig = (parse(stdout) as FileConfig) || { http: { routers: {}, services: {} }, }; return parsedConfig; @@ -248,7 +248,7 @@ export const writeTraefikConfig = ( try { const { DYNAMIC_TRAEFIK_PATH } = paths(); const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`); - const yamlStr = dump(traefikConfig); + const yamlStr = stringify(traefikConfig); fs.writeFileSync(configPath, yamlStr, "utf8"); } catch (e) { console.error("Error saving the YAML config file:", e); @@ -263,7 +263,7 @@ export const writeTraefikConfigRemote = async ( try { const { DYNAMIC_TRAEFIK_PATH } = paths(true); const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`); - const yamlStr = dump(traefikConfig); + const yamlStr = stringify(traefikConfig); await execAsyncRemote(serverId, `echo '${yamlStr}' > ${configPath}`); } catch (e) { console.error("Error saving the YAML config file:", e); diff --git a/packages/server/src/utils/traefik/middleware.ts b/packages/server/src/utils/traefik/middleware.ts index 1254e1f51..4897b94ee 100644 --- a/packages/server/src/utils/traefik/middleware.ts +++ b/packages/server/src/utils/traefik/middleware.ts @@ -2,7 +2,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { paths } from "@dokploy/server/constants"; import type { Domain } from "@dokploy/server/services/domain"; -import { dump, load } from "js-yaml"; +import { parse, stringify } from "yaml"; import type { ApplicationNested } from "../builders"; import { execAsyncRemote } from "../process/execAsync"; import { writeTraefikConfigRemote } from "./application"; @@ -76,7 +76,7 @@ export const loadMiddlewares = () => { throw new Error(`File not found: ${configPath}`); } const yamlStr = readFileSync(configPath, "utf8"); - const config = load(yamlStr) as T; + const config = parse(yamlStr) as T; return config; }; @@ -94,7 +94,7 @@ export const loadRemoteMiddlewares = async (serverId: string) => { console.error(`Error: ${stderr}`); throw new Error(`File not found: ${configPath}`); } - const config = load(stdout) as FileConfig; + const config = parse(stdout) as FileConfig; return config; } catch (_) { throw new Error(`File not found: ${configPath}`); @@ -103,7 +103,7 @@ export const loadRemoteMiddlewares = async (serverId: string) => { export const writeMiddleware = (config: T) => { const { DYNAMIC_TRAEFIK_PATH } = paths(); const configPath = join(DYNAMIC_TRAEFIK_PATH, "middlewares.yml"); - const newYamlContent = dump(config); + const newYamlContent = stringify(config); writeFileSync(configPath, newYamlContent, "utf8"); }; diff --git a/packages/server/src/utils/traefik/web-server.ts b/packages/server/src/utils/traefik/web-server.ts index 1534e2f1c..02472a583 100644 --- a/packages/server/src/utils/traefik/web-server.ts +++ b/packages/server/src/utils/traefik/web-server.ts @@ -2,7 +2,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { paths } from "@dokploy/server/constants"; import type { User } from "@dokploy/server/services/user"; -import { dump, load } from "js-yaml"; +import { parse, stringify } from "yaml"; import { loadOrCreateConfig, removeTraefikConfig, @@ -79,13 +79,13 @@ export const updateLetsEncryptEmail = (newEmail: string | null) => { const { MAIN_TRAEFIK_PATH } = paths(); const configPath = join(MAIN_TRAEFIK_PATH, "traefik.yml"); const configContent = readFileSync(configPath, "utf8"); - const config = load(configContent) as MainTraefikConfig; + const config = parse(configContent) as MainTraefikConfig; if (config?.certificatesResolvers?.letsencrypt?.acme) { config.certificatesResolvers.letsencrypt.acme.email = newEmail; } else { throw new Error("Invalid Let's Encrypt configuration structure."); } - const newYamlContent = dump(config); + const newYamlContent = stringify(config); writeFileSync(configPath, newYamlContent, "utf8"); } catch (error) { throw error; diff --git a/packages/server/src/utils/volume-backups/utils.ts b/packages/server/src/utils/volume-backups/utils.ts index 5b55c240c..b6a34e2aa 100644 --- a/packages/server/src/utils/volume-backups/utils.ts +++ b/packages/server/src/utils/volume-backups/utils.ts @@ -1,15 +1,17 @@ -import { findVolumeBackupById } from "@dokploy/server/services/volume-backups"; -import { scheduledJobs, scheduleJob } from "node-schedule"; +import path from "node:path"; +import { paths } from "@dokploy/server/constants"; import { createDeploymentVolumeBackup, updateDeploymentStatus, } from "@dokploy/server/services/deployment"; +import { findVolumeBackupById } from "@dokploy/server/services/volume-backups"; import { execAsync, execAsyncRemote, } from "@dokploy/server/utils/process/execAsync"; -import { backupVolume } from "./backup"; +import { scheduledJobs, scheduleJob } from "node-schedule"; import { getS3Credentials, normalizeS3Path } from "../backups/utils"; +import { backupVolume } from "./backup"; export const scheduleVolumeBackup = async (volumeBackupId: string) => { const volumeBackup = await findVolumeBackupById(volumeBackupId); @@ -76,7 +78,20 @@ export const runVolumeBackup = async (volumeBackupId: string) => { await updateDeploymentStatus(deployment.deploymentId, "done"); } catch (error) { + const { VOLUME_BACKUPS_PATH } = paths(!!serverId); + const volumeBackupPath = path.join( + VOLUME_BACKUPS_PATH, + volumeBackup.appName, + ); + // delete all the .tar files + const command = `rm -rf ${volumeBackupPath}/*.tar`; + if (serverId) { + await execAsyncRemote(serverId, command); + } else { + await execAsync(command); + } await updateDeploymentStatus(deployment.deploymentId, "error"); + console.error(error); } }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94dedb322..6df6799ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -325,9 +325,6 @@ importers: js-cookie: specifier: ^3.0.5 version: 3.0.5 - js-yaml: - specifier: 4.1.0 - version: 4.1.0 lodash: specifier: 4.17.21 version: 4.17.21 @@ -448,6 +445,9 @@ importers: xterm-addon-fit: specifier: ^0.8.0 version: 0.8.0(xterm@5.3.0) + yaml: + specifier: 2.8.1 + version: 2.8.1 zod: specifier: ^3.25.32 version: 3.25.32 @@ -464,9 +464,6 @@ importers: '@types/js-cookie': specifier: ^3.0.6 version: 3.0.6 - '@types/js-yaml': - specifier: 4.0.9 - version: 4.0.9 '@types/lodash': specifier: 4.17.4 version: 4.17.4 @@ -681,9 +678,6 @@ importers: hi-base32: specifier: ^0.5.1 version: 0.5.1 - js-yaml: - specifier: 4.1.0 - version: 4.1.0 lodash: specifier: 4.17.21 version: 4.17.21 @@ -747,6 +741,9 @@ importers: ws: specifier: 8.16.0 version: 8.16.0 + yaml: + specifier: 2.8.1 + version: 2.8.1 zod: specifier: ^3.25.32 version: 3.25.32 @@ -760,9 +757,6 @@ importers: '@types/dockerode': specifier: 3.3.23 version: 3.3.23 - '@types/js-yaml': - specifier: 4.0.9 - version: 4.0.9 '@types/lodash': specifier: 4.17.4 version: 4.17.4 @@ -3965,9 +3959,6 @@ packages: '@types/js-cookie@3.0.6': resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} - '@types/js-yaml@4.0.9': - resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} - '@types/jsonwebtoken@9.0.9': resolution: {integrity: sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==} @@ -7783,6 +7774,11 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} @@ -11223,8 +11219,6 @@ snapshots: '@types/js-cookie@3.0.6': {} - '@types/js-yaml@4.0.9': {} - '@types/jsonwebtoken@9.0.9': dependencies: '@types/ms': 2.1.0 @@ -14032,7 +14026,7 @@ snapshots: postcss-load-config@4.0.2(postcss@8.5.3): dependencies: lilconfig: 3.1.3 - yaml: 2.8.0 + yaml: 2.8.1 optionalDependencies: postcss: 8.5.3 @@ -15353,6 +15347,8 @@ snapshots: yaml@2.8.0: {} + yaml@2.8.1: {} + yargs-parser@18.1.3: dependencies: camelcase: 5.3.1