diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 2ac54229..e9591f3c 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -12,7 +12,7 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: 20.9.0 + node-version: 20.16.0 cache: "pnpm" - run: pnpm install --frozen-lockfile - run: pnpm run server:build @@ -26,7 +26,7 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: 20.9.0 + node-version: 20.16.0 cache: "pnpm" - run: pnpm install --frozen-lockfile - run: pnpm run server:build @@ -39,7 +39,7 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: 20.9.0 + node-version: 20.16.0 cache: "pnpm" - run: pnpm install --frozen-lockfile - run: pnpm run server:build diff --git a/.nvmrc b/.nvmrc index 43bff1f8..593cb75b 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.9.0 \ No newline at end of file +20.16.0 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a69fa686..0ac5a358 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,7 +52,7 @@ feat: add new feature Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch. -We use Node v20.9.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.9.0 && nvm use` in the root directory. +We use Node v20.16.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.16.0 && nvm use` in the root directory. ```bash git clone https://github.com/dokploy/dokploy.git diff --git a/Dockerfile b/Dockerfile index c2a9fd89..00043b0c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,7 @@ WORKDIR /app # Set production ENV NODE_ENV=production -RUN apt-get update && apt-get install -y curl unzip zip apache2-utils iproute2 rsync && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y curl unzip zip apache2-utils iproute2 rsync git-lfs && git lfs install && rm -rf /var/lib/apt/lists/* # Copy only the necessary files COPY --from=build /prod/dokploy/.next ./.next diff --git a/README.md b/README.md index 90c651b0..d192d6f7 100644 --- a/README.md +++ b/README.md @@ -148,19 +148,6 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com). Watch the video - - ## Contributing Check out the [Contributing Guide](CONTRIBUTING.md) for more information. diff --git a/apps/dokploy/.nvmrc b/apps/dokploy/.nvmrc index 43bff1f8..593cb75b 100644 --- a/apps/dokploy/.nvmrc +++ b/apps/dokploy/.nvmrc @@ -1 +1 @@ -20.9.0 \ No newline at end of file +20.16.0 \ No newline at end of file diff --git a/apps/dokploy/Dockerfile b/apps/dokploy/Dockerfile deleted file mode 100644 index 0537b03e..00000000 --- a/apps/dokploy/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -FROM node:18-slim AS base -ENV PNPM_HOME="/pnpm" -ENV PATH="$PNPM_HOME:$PATH" -RUN corepack enable - -FROM base AS build -COPY . /usr/src/app -WORKDIR /usr/src/app - - -RUN apt-get update && apt-get install -y python3 make g++ git git-lfs && git lfs install && rm -rf /var/lib/apt/lists/* - -# Install dependencies -RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile - -# Build only the dokploy app -RUN pnpm run dokploy:build - -# Deploy only the dokploy app -RUN pnpm deploy --filter=dokploy --prod /prod/dokploy - -FROM base AS dokploy -COPY --from=build /prod/dokploy /prod/dokploy -WORKDIR /prod/dokploy -EXPOSE 3000 -CMD [ "pnpm", "start" ] \ No newline at end of file diff --git a/apps/dokploy/__test__/drop/drop.test.test.ts b/apps/dokploy/__test__/drop/drop.test.test.ts index 74a4eb66..b18d7b4b 100644 --- a/apps/dokploy/__test__/drop/drop.test.test.ts +++ b/apps/dokploy/__test__/drop/drop.test.test.ts @@ -105,6 +105,7 @@ const baseApp: ApplicationNested = { ports: [], projectId: "", publishDirectory: null, + isStaticSpa: null, redirects: [], refreshToken: "", registry: null, @@ -149,67 +150,68 @@ describe("unzipDrop using real zip files", () => { } finally { } }); - - it("should correctly extract a zip with a single root folder and a subfolder", async () => { - baseApp.appName = "folderwithfile"; - // const appName = "folderwithfile"; - const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); - const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip"); - - const zipBuffer = zip.toBuffer(); - const file = new File([zipBuffer], "single.zip"); - await unzipDrop(file, baseApp); - - const files = await fs.readdir(outputPath, { withFileTypes: true }); - expect(files.some((f) => f.name === "folder1.txt")).toBe(true); - }); - - it("should correctly extract a zip with multiple root folders", async () => { - baseApp.appName = "two-folders"; - // const appName = "two-folders"; - const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); - const zip = new AdmZip("./__test__/drop/zips/two-folders.zip"); - - const zipBuffer = zip.toBuffer(); - const file = new File([zipBuffer], "single.zip"); - await unzipDrop(file, baseApp); - - const files = await fs.readdir(outputPath, { withFileTypes: true }); - - expect(files.some((f) => f.name === "folder1")).toBe(true); - expect(files.some((f) => f.name === "folder2")).toBe(true); - }); - - it("should correctly extract a zip with a single root with a file", async () => { - baseApp.appName = "nested"; - // const appName = "nested"; - const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); - const zip = new AdmZip("./__test__/drop/zips/nested.zip"); - - const zipBuffer = zip.toBuffer(); - const file = new File([zipBuffer], "single.zip"); - await unzipDrop(file, baseApp); - - const files = await fs.readdir(outputPath, { withFileTypes: true }); - - expect(files.some((f) => f.name === "folder1")).toBe(true); - expect(files.some((f) => f.name === "folder2")).toBe(true); - expect(files.some((f) => f.name === "folder3")).toBe(true); - }); - - it("should correctly extract a zip with a single root with a folder", async () => { - baseApp.appName = "folder-with-sibling-file"; - // const appName = "folder-with-sibling-file"; - const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); - const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip"); - - const zipBuffer = zip.toBuffer(); - const file = new File([zipBuffer], "single.zip"); - await unzipDrop(file, baseApp); - - const files = await fs.readdir(outputPath, { withFileTypes: true }); - - expect(files.some((f) => f.name === "folder1")).toBe(true); - expect(files.some((f) => f.name === "test.txt")).toBe(true); - }); }); + +// it("should correctly extract a zip with a single root folder and a subfolder", async () => { +// baseApp.appName = "folderwithfile"; +// // const appName = "folderwithfile"; +// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); +// const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip"); + +// const zipBuffer = zip.toBuffer(); +// const file = new File([zipBuffer], "single.zip"); +// await unzipDrop(file, baseApp); + +// const files = await fs.readdir(outputPath, { withFileTypes: true }); +// expect(files.some((f) => f.name === "folder1.txt")).toBe(true); +// }); + +// it("should correctly extract a zip with multiple root folders", async () => { +// baseApp.appName = "two-folders"; +// // const appName = "two-folders"; +// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); +// const zip = new AdmZip("./__test__/drop/zips/two-folders.zip"); + +// const zipBuffer = zip.toBuffer(); +// const file = new File([zipBuffer], "single.zip"); +// await unzipDrop(file, baseApp); + +// const files = await fs.readdir(outputPath, { withFileTypes: true }); + +// expect(files.some((f) => f.name === "folder1")).toBe(true); +// expect(files.some((f) => f.name === "folder2")).toBe(true); +// }); + +// it("should correctly extract a zip with a single root with a file", async () => { +// baseApp.appName = "nested"; +// // const appName = "nested"; +// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); +// const zip = new AdmZip("./__test__/drop/zips/nested.zip"); + +// const zipBuffer = zip.toBuffer(); +// const file = new File([zipBuffer], "single.zip"); +// await unzipDrop(file, baseApp); + +// const files = await fs.readdir(outputPath, { withFileTypes: true }); + +// expect(files.some((f) => f.name === "folder1")).toBe(true); +// expect(files.some((f) => f.name === "folder2")).toBe(true); +// expect(files.some((f) => f.name === "folder3")).toBe(true); +// }); + +// it("should correctly extract a zip with a single root with a folder", async () => { +// baseApp.appName = "folder-with-sibling-file"; +// // const appName = "folder-with-sibling-file"; +// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); +// const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip"); + +// const zipBuffer = zip.toBuffer(); +// const file = new File([zipBuffer], "single.zip"); +// await unzipDrop(file, baseApp); + +// const files = await fs.readdir(outputPath, { withFileTypes: true }); + +// expect(files.some((f) => f.name === "folder1")).toBe(true); +// expect(files.some((f) => f.name === "test.txt")).toBe(true); +// }); +// }); diff --git a/apps/dokploy/__test__/traefik/traefik.test.ts b/apps/dokploy/__test__/traefik/traefik.test.ts index 5437e64d..6c136b25 100644 --- a/apps/dokploy/__test__/traefik/traefik.test.ts +++ b/apps/dokploy/__test__/traefik/traefik.test.ts @@ -85,6 +85,7 @@ const baseApp: ApplicationNested = { ports: [], projectId: "", publishDirectory: null, + isStaticSpa: null, redirects: [], refreshToken: "", registry: null, diff --git a/apps/dokploy/__test__/utils/backups.test.ts b/apps/dokploy/__test__/utils/backups.test.ts index c7bc310c..2c1e5dec 100644 --- a/apps/dokploy/__test__/utils/backups.test.ts +++ b/apps/dokploy/__test__/utils/backups.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, test } from "vitest"; import { normalizeS3Path } from "@dokploy/server/utils/backups/utils"; +import { describe, expect, test } from "vitest"; describe("normalizeS3Path", () => { test("should handle empty and whitespace-only prefix", () => { diff --git a/apps/dokploy/components/dashboard/application/build/show.tsx b/apps/dokploy/components/dashboard/application/build/show.tsx index 9535a318..291026d4 100644 --- a/apps/dokploy/components/dashboard/application/build/show.tsx +++ b/apps/dokploy/components/dashboard/application/build/show.tsx @@ -2,6 +2,7 @@ import { AlertBlock } from "@/components/shared/alert-block"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Checkbox } from "@/components/ui/checkbox"; import { Form, FormControl, @@ -63,10 +64,11 @@ const mySchema = z.discriminatedUnion("buildType", [ publishDirectory: z.string().optional(), }), z.object({ - buildType: z.literal(BuildType.static), + buildType: z.literal(BuildType.railpack), }), z.object({ - buildType: z.literal(BuildType.railpack), + buildType: z.literal(BuildType.static), + isStaticSpa: z.boolean().default(false), }), ]); @@ -83,6 +85,7 @@ interface ApplicationData { dockerBuildStage?: string | null; herokuVersion?: string | null; publishDirectory?: string | null; + isStaticSpa?: boolean | null; } function isValidBuildType(value: string): value is BuildType { @@ -115,16 +118,18 @@ const resetData = (data: ApplicationData): AddTemplate => { case BuildType.static: return { buildType: BuildType.static, + isStaticSpa: data.isStaticSpa ?? false, }; case BuildType.railpack: return { buildType: BuildType.railpack, }; - default: + default: { const buildType = data.buildType as BuildType; return { buildType, } as AddTemplate; + } } }; @@ -174,6 +179,8 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { data.buildType === BuildType.heroku_buildpacks ? data.herokuVersion : null, + isStaticSpa: + data.buildType === BuildType.static ? data.isStaticSpa : null, }) .then(async () => { toast.success("Build type saved"); @@ -364,6 +371,30 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { )} /> )} + {buildType === BuildType.static && ( + ( + + +
+ + + Single Page Application (SPA) + +
+
+ +
+ )} + /> + )}