diff --git a/src/cli/install-commitlint.ts b/src/cli/install-commitlint.ts index a6e30686..5692532e 100644 --- a/src/cli/install-commitlint.ts +++ b/src/cli/install-commitlint.ts @@ -13,7 +13,7 @@ const packageJson = new PackageJson(); export const installCommitLint = async () => { await installHusky(); - await packageJson.install("@commitlint/cli", { version: "20", dev: true }); + await packageJson.install("@commitlint/cli", { version: "^20", dev: true }); await writeFile( path.join(root, ".husky/commit-msg"), diff --git a/src/cli/templates/node-setup-ci.ts b/src/cli/templates/node-setup-ci.ts index 17e244d1..1cafe15b 100644 --- a/src/cli/templates/node-setup-ci.ts +++ b/src/cli/templates/node-setup-ci.ts @@ -14,7 +14,7 @@ export const nodeSetupCi = ({ uses: actions/checkout@v6 with: fetch-depth: 0 - ${manager.name === "pnpm" ? pnpmSetupCi({ pnpmVersion }) : ""} +${manager.name === "pnpm" ? pnpmSetupCi({ pnpmVersion }) : ""} - name: Setup node uses: actions/setup-node@v6 with: diff --git a/tests/integration.test.ts b/tests/integration.test.ts index 51b56f84..921529d6 100644 --- a/tests/integration.test.ts +++ b/tests/integration.test.ts @@ -3,6 +3,76 @@ import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { getCurrentPackageManager } from "./utils/package-manager"; import { TestEnvironment } from "./utils/test-environment"; +const expectGeneratedConfigToBeFormatted = async ({ + env, + appPath, + expectedGeneratedFiles, +}: { + env: TestEnvironment; + appPath: string; + expectedGeneratedFiles: string[]; +}) => { + const missingFiles = expectedGeneratedFiles.filter( + (filePath) => !env.fileExists(appPath, filePath), + ); + expect( + missingFiles, + `Missing generated files:\n${missingFiles.join("\n")}`, + ).toEqual([]); + + const generatedFilesThatExist = expectedGeneratedFiles.filter((filePath) => + env.fileExists(appPath, filePath), + ); + const prettierCheckResult = await env.runPrettierCheckWithFallback( + appPath, + generatedFilesThatExist, + ); + expect(prettierCheckResult.skipped, prettierCheckResult.output).toBe(false); + expect(prettierCheckResult.success, prettierCheckResult.output).toBe(true); +}; + +const expectCommitlintHookToAllowMessage = async ({ + env, + appPath, + message, +}: { + env: TestEnvironment; + appPath: string; + message: string; +}) => { + env.writeFile( + appPath, + "commitlint-hook-test.txt", + `commitlint hook test ${Date.now()}\n`, + ); + const commitResult = await env.commitWithMessage(appPath, message); + expect(commitResult.success, commitResult.output).toBe(true); +}; + +const expectCommitlintHookToRejectMessage = async ({ + env, + appPath, + message, +}: { + env: TestEnvironment; + appPath: string; + message: string; +}) => { + env.writeFile( + appPath, + "commitlint-hook-test-invalid.txt", + `commitlint hook invalid test ${Date.now()}\n`, + ); + const commitResult = await env.commitWithMessage(appPath, message); + expect(commitResult.success, commitResult.output).toBe(false); + expect( + /subject-empty|type-empty|commit-msg script failed/i.test( + commitResult.output, + ), + commitResult.output, + ).toBe(true); +}; + describe("Next.js Integration Tests", () => { let testEnv: TestEnvironment; const packageManager = getCurrentPackageManager(); @@ -27,6 +97,18 @@ describe("Next.js Integration Tests", () => { const output = await env.runSolvroConfig(appPath, ["--all", "--force"]); expect(output).toContain("Konfiguracja zakończona pomyślnie"); + await expectGeneratedConfigToBeFormatted({ + env, + appPath, + expectedGeneratedFiles: [ + "eslint.config.js", + "package.json", + ".github/workflows/ci.yml", + ".github/dependabot.yml", + ".commitlintrc.js", + ], + }); + const prettierResult = await env.runPrettier(appPath, true); expect(prettierResult.success).toBe(true); @@ -77,6 +159,18 @@ describe("NestJS Integration Tests", () => { const output = await env.runSolvroConfig(appPath, ["--all", "--force"]); expect(output).toContain("Konfiguracja zakończona pomyślnie"); + await expectGeneratedConfigToBeFormatted({ + env, + appPath, + expectedGeneratedFiles: [ + "eslint.config.mjs", + "package.json", + ".github/workflows/ci.yml", + ".github/dependabot.yml", + ".commitlintrc.js", + ], + }); + const prettierResult = await env.runPrettier(appPath, true); expect(prettierResult.success).toBe(true); @@ -127,6 +221,18 @@ describe("Vite Integration Tests", () => { const output = await env.runSolvroConfig(appPath, ["--all", "--force"]); expect(output).toContain("Konfiguracja zakończona pomyślnie"); + await expectGeneratedConfigToBeFormatted({ + env, + appPath, + expectedGeneratedFiles: [ + "eslint.config.js", + "package.json", + ".github/workflows/ci.yml", + ".github/dependabot.yml", + ".commitlintrc.js", + ], + }); + const prettierResult = await env.runPrettier(appPath, true); expect(prettierResult.success).toBe(true); @@ -154,3 +260,60 @@ describe("Vite Integration Tests", () => { expect(packageJson).toContain('"prettier": "@solvro/config/prettier"'); }); }); + +describe("Commitlint Integration Tests", () => { + let testEnv: TestEnvironment; + const packageManager = getCurrentPackageManager(); + + beforeEach(async () => { + testEnv = new TestEnvironment("commitlint-integration", packageManager); + }); + + afterEach(() => { + testEnv?.cleanup(); + }); + + test(`should allow valid commit message through husky commit-msg hook with ${packageManager.name}`, async () => { + const env = testEnv; + await env.setup(); + + const appPath = await env.createNextjsApp("commitlint-test-app"); + await env.installSolvroConfig(appPath); + await env.initGitRepo(appPath); + + const output = await env.runSolvroConfig(appPath, [ + "--commitlint", + "--force", + ]); + expect(output).toContain("Konfiguracja zakończona pomyślnie"); + + await expectGeneratedConfigToBeFormatted({ + env, + appPath, + expectedGeneratedFiles: ["package.json", ".commitlintrc.js"], + }); + + const packageJson = JSON.parse(env.readFile(appPath, "package.json")) as { + scripts?: Record; + }; + packageJson.scripts = packageJson.scripts ?? {}; + packageJson.scripts.test = packageJson.scripts.test ?? "true"; + env.writeFile( + appPath, + "package.json", + JSON.stringify(packageJson, null, 2), + ); + + await expectCommitlintHookToAllowMessage({ + env, + appPath, + message: "chore: test commit", + }); + + await expectCommitlintHookToRejectMessage({ + env, + appPath, + message: "invalid commit message", + }); + }); +}); diff --git a/tests/utils/test-environment.ts b/tests/utils/test-environment.ts index 620e341f..88cf2e95 100644 --- a/tests/utils/test-environment.ts +++ b/tests/utils/test-environment.ts @@ -404,6 +404,83 @@ export class TestEnvironment { } } + async runPrettierCheckWithFallback( + appPath: string, + paths: string[] = ["."], + ): Promise<{ success: boolean; output: string; skipped: boolean }> { + const packageJson = JSON.parse(this.readFile(appPath, "package.json")) as { + dependencies?: Record; + devDependencies?: Record; + }; + const hasPrettier = + packageJson.dependencies?.prettier != null || + packageJson.devDependencies?.prettier != null; + + if (paths.length === 0) { + return { + success: true, + output: "Skipped prettier check: no files selected.", + skipped: true, + }; + } + + const prettierCommand: keyof PackageManagerConfig = hasPrettier + ? "localExecute" + : "downloadExecute"; + + const prettierArgs = ["prettier", "--check", ...paths]; + + if (!hasPrettier && this.packageManager.name === "npm") { + prettierArgs.unshift("--yes"); + } + + try { + const { stdout, stderr } = await this.execute({ + command: prettierCommand, + args: prettierArgs, + cwd: appPath, + label: hasPrettier ? "prettier-check" : "prettier-check-dlx", + }); + return { success: true, output: stdout + stderr, skipped: false }; + } catch (error: any) { + return { + success: false, + output: (error.stdout || "") + (error.stderr || ""), + skipped: false, + }; + } + } + + async commitWithMessage( + appPath: string, + message: string, + ): Promise<{ success: boolean; output: string }> { + try { + const { stdout: addStdout, stderr: addStderr } = await execWithLogging( + "git", + ["add", "."], + { cwd: appPath }, + "git-add-for-commit", + ); + const { stdout: commitStdout, stderr: commitStderr } = + await execWithLogging( + "git", + ["commit", "-m", message], + { cwd: appPath }, + "git-commit-with-message", + ); + return { + success: true, + output: addStdout + addStderr + commitStdout + commitStderr, + }; + } catch (error: any) { + return { + success: false, + output: (error.stdout || "") + (error.stderr || ""), + }; + } + } + async buildNextjsApp( appPath: string, ): Promise<{ success: boolean; output: string }> {