Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions packages/create-voltagent-app/src/__tests__/templates.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import type { ProjectOptions } from "../types";
import { getBaseTemplates } from "../utils/templates";

const baseOptions: ProjectOptions = {
projectName: "my-agent",
typescript: true,
packageManager: "npm",
features: [],
};

const renderDockerfile = (packageManager: ProjectOptions["packageManager"]) => {
const template = getBaseTemplates().find((t) => t.targetPath === "Dockerfile");
if (!template?.transform) {
throw new Error("Dockerfile template with a transform not found");
}
const content = readFileSync(template.sourcePath, "utf-8");
return template.transform(content, { ...baseOptions, packageManager });
};

describe("Dockerfile template", () => {
it("renders npm install layers for npm projects", () => {
const dockerfile = renderDockerfile("npm");
expect(dockerfile).toContain("RUN npm ci\n");
expect(dockerfile).toContain("RUN npm ci --omit=dev");
expect(dockerfile).not.toContain("yarn.lock");
expect(dockerfile).not.toContain("pnpm-lock.yaml");
});

it.each([
["npm", "", "RUN npm ci", "RUN npm ci --omit=dev"],
[
"yarn",
"yarn.lock",
"RUN yarn install --frozen-lockfile",
"RUN yarn install --frozen-lockfile --production",
],
[
"pnpm",
"pnpm-lock.yaml",
"RUN npm install -g pnpm && pnpm install --frozen-lockfile",
"RUN npm install -g pnpm && pnpm install --frozen-lockfile --prod",
],
[
"bun",
"bun.lock",
"RUN npm install -g bun && bun install --frozen-lockfile",
"RUN npm install -g bun && bun install --frozen-lockfile --production",
],
] as const)(
"renders only %s dependency layers",
(manager, lockfile, builderCommand, runtimeCommand) => {
const dockerfile = renderDockerfile(manager);
const [builder, runtime] = dockerfile.split("# Production stage");
expect(builder.split("\n")).toContain(builderCommand);
expect(runtime.split("\n")).toContain(runtimeCommand);
for (const candidate of ["yarn.lock", "pnpm-lock.yaml", "bun.lock"]) {
if (candidate === lockfile) {
expect(builder).toContain(`COPY ${candidate}* ./`);
expect(runtime).toContain(`COPY ${candidate}* ./`);
} else {
expect(dockerfile).not.toContain(candidate);
}
}
},
);

it("keeps the package.json copy in both stages", () => {
const dockerfile = renderDockerfile("npm");
expect(dockerfile.match(/COPY package\*\.json \.\//g)?.length).toBe(2);
});
});
34 changes: 34 additions & 0 deletions packages/create-voltagent-app/src/utils/templates.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import path from "node:path";
import {
AI_PROVIDER_CONFIG,
type PackageManager,
type ProjectOptions,
SERVER_CONFIG,
type TemplateFile,
Expand Down Expand Up @@ -134,6 +135,39 @@ export const getBaseTemplates = (): TemplateFile[] => {
{
sourcePath: path.join(TEMPLATES_DIR, "base/Dockerfile.template"),
targetPath: "Dockerfile",
transform: (content: string, options: ProjectOptions) => {
// Docker validates COPY sources before any RUN fallback executes, so
// the dependency layers must only reference the lockfile of the
// package manager selected in the wizard.
const packageManager = options.packageManager;
const lockfileCopy: Record<PackageManager, string> = {
npm: "",
yarn: "COPY yarn.lock* ./",
pnpm: "COPY pnpm-lock.yaml* ./",
bun: "COPY bun.lock* ./",
};
const installCommand: Record<PackageManager, { builder: string; runtime: string }> = {
npm: { builder: "RUN npm ci", runtime: "RUN npm ci --omit=dev" },
yarn: {
builder: "RUN yarn install --frozen-lockfile",
runtime: "RUN yarn install --frozen-lockfile --production",
},
pnpm: {
builder: "RUN npm install -g pnpm && pnpm install --frozen-lockfile",
runtime: "RUN npm install -g pnpm && pnpm install --frozen-lockfile --prod",
},
bun: {
builder: "RUN npm install -g bun && bun install --frozen-lockfile",
runtime: "RUN npm install -g bun && bun install --frozen-lockfile --production",
},
};
const lockfile = lockfileCopy[packageManager];

return content
.replace(/\n\{\{dockerLockfileCopy\}\}/g, lockfile ? `\n${lockfile}` : "")
.replace(/\{\{dockerInstallBuilder\}\}/g, installCommand[packageManager].builder)
.replace(/\{\{dockerInstallProd\}\}/g, installCommand[packageManager].runtime);
},
},
{
sourcePath: path.join(TEMPLATES_DIR, "base/.dockerignore.template"),
Expand Down
14 changes: 4 additions & 10 deletions packages/create-voltagent-app/templates/base/Dockerfile.template
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,10 @@ WORKDIR /app

# Copy package files
COPY package*.json ./
COPY yarn.lock* ./
COPY pnpm-lock.yaml* ./
{{dockerLockfileCopy}}

# Install dependencies based on lock file
RUN if [ -f yarn.lock ]; then yarn install --frozen-lockfile; \
elif [ -f pnpm-lock.yaml ]; then npm install -g pnpm && pnpm install --frozen-lockfile; \
else npm ci; fi
{{dockerInstallBuilder}}

# Copy source code
COPY . .
Expand All @@ -35,13 +32,10 @@ WORKDIR /app

# Copy package files
COPY package*.json ./
COPY yarn.lock* ./
COPY pnpm-lock.yaml* ./
{{dockerLockfileCopy}}

# Install production dependencies only
RUN if [ -f yarn.lock ]; then yarn install --frozen-lockfile --production; \
elif [ -f pnpm-lock.yaml ]; then npm install -g pnpm && pnpm install --frozen-lockfile --prod; \
else npm ci --omit=dev; fi
{{dockerInstallProd}}

# Copy built application from builder stage
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
Expand Down