Skip to content

Commit 9cd0279

Browse files
committed
feat: apply hooks on installation
Signed-off-by: Emilien Escalle <emilien.escalle@escemi.com>
1 parent ed268d2 commit 9cd0279

10 files changed

Lines changed: 377 additions & 104 deletions

packages/core/src/__snapshots__/core.e2e.spec.ts.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ Applying migration "core - 20260604100000-migrate-to-vitest"...
2222
Migration "core - 20260604100000-migrate-to-vitest" applied!
2323
Symlinking dev dependencies...
2424
- Symlinking @biomejs/biome
25-
- Symlinking @vitest/coverage-v8
2625
- Symlinking @commitlint/cli
2726
- Symlinking @commitlint/config-conventional
2827
- Symlinking @types/node
28+
- Symlinking @vitest/coverage-v8
2929
- Symlinking vitest
3030
Symlinking dev dependencies done!
3131
Resolving peer dependencies...
@@ -60,10 +60,10 @@ Applying migration "core - 20260604100000-migrate-to-vitest"...
6060
Migration "core - 20260604100000-migrate-to-vitest" applied!
6161
Symlinking dev dependencies...
6262
- Symlinking @biomejs/biome
63-
- Symlinking @vitest/coverage-v8
6463
- Symlinking @commitlint/cli
6564
- Symlinking @commitlint/config-conventional
6665
- Symlinking @types/node
66+
- Symlinking @vitest/coverage-v8
6767
- Symlinking vitest
6868
Symlinking dev dependencies done!
6969
Resolving peer dependencies...

packages/core/src/install/migrations/20201024173398-init.ts

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,32 @@
11
import { PROJECT_NAME } from "../../constants";
2-
import { GitService } from "../../services/GitService";
2+
import type { ManagedGitHook } from "../../services/HooksService";
33
import type { MigrationUpFunction } from "../../services/MigrationsService";
44
import { PackageJson } from "../../services/PackageJson";
55
import { PackageManagerService } from "../../services/PackageManagerService";
66

7+
function getPackageManagerCommand(absoluteProjectDir: string): string {
8+
return PackageManagerService.detectPackageManager(absoluteProjectDir);
9+
}
10+
11+
export const hooks: ManagedGitHook[] = [
12+
{
13+
name: "pre-commit",
14+
command:
15+
"npx --no-install lint-staged && npx --no-install pretty-quick --staged",
16+
},
17+
{
18+
name: "commit-msg",
19+
command: "npx --no-install commitlint --edit $1",
20+
},
21+
{
22+
name: "pre-push",
23+
command: (absoluteProjectDir: string) => {
24+
const packageManager = getPackageManagerCommand(absoluteProjectDir);
25+
return `${packageManager} run lint && ${packageManager} run build && ${packageManager} run test`;
26+
},
27+
},
28+
];
29+
730
export const up: MigrationUpFunction = async (
831
absoluteProjectDir: string,
932
): Promise<void> => {
@@ -90,25 +113,4 @@ export const up: MigrationUpFunction = async (
90113
scripts,
91114
jest,
92115
});
93-
94-
// Install Git hooks (only if we are in a git repository)
95-
const isGitRepository = await GitService.isGitRepository(absoluteProjectDir);
96-
97-
if (isGitRepository) {
98-
const gitHooks = {
99-
"pre-commit":
100-
"npx --no-install lint-staged && npx --no-install pretty-quick --staged",
101-
"commit-msg": "npx --no-install commitlint --edit $1",
102-
"pre-push": `${packageManager} run lint && ${packageManager} run build && ${packageManager} run test`,
103-
};
104-
105-
for (const gitHookName of Object.keys(gitHooks)) {
106-
const gitHookCommand = gitHooks[gitHookName as keyof typeof gitHooks];
107-
await GitService.addGitHook(
108-
absoluteProjectDir,
109-
gitHookName,
110-
gitHookCommand,
111-
);
112-
}
113-
}
114116
};

packages/core/src/install/migrations/20260311120000-migrate-to-biome.spec.ts

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,8 @@
1-
import {
2-
existsSync,
3-
mkdirSync,
4-
readFileSync,
5-
rmSync,
6-
unlinkSync,
7-
writeFileSync,
8-
} from "node:fs";
1+
import { existsSync, mkdirSync, rmSync, unlinkSync } from "node:fs";
92
import { join } from "node:path";
103

114
import { CmdService } from "../../services/CmdService";
125
import { FileService } from "../../services/FileService";
13-
import { GitService } from "../../services/GitService";
146
import {
157
PackageJson,
168
type PackageJsonContent,
@@ -19,10 +11,8 @@ import {
1911
createProjectForTestFile,
2012
deleteTestProject,
2113
} from "../../tests/test-project";
22-
import { up } from "./20260311120000-migrate-to-biome";
14+
import { hooks, up } from "./20260311120000-migrate-to-biome";
2315

24-
const OLD_PRE_COMMIT_COMMAND =
25-
"npx --no-install lint-staged && npx --no-install pretty-quick --staged";
2616
const BIOME_SCHEMA_URL_PATTERN =
2717
/https:\/\/biomejs\.dev\/schemas\/[^"]+\/schema\.json/;
2818

@@ -92,20 +82,6 @@ describe("Migration 20260311120000-migrate-to-biome", () => {
9282
`,
9383
);
9484

95-
const preCommitHookPath = join(
96-
testProjectDir,
97-
".git",
98-
"hooks",
99-
"pre-commit",
100-
);
101-
writeFileSync(
102-
preCommitHookPath,
103-
GitService.GIT_HOOK_TEMPLATE.replace(
104-
"%gitHookCommand%",
105-
OLD_PRE_COMMIT_COMMAND,
106-
),
107-
);
108-
10985
await up(testProjectDir);
11086

11187
expect(
@@ -121,7 +97,6 @@ describe("Migration 20260311120000-migrate-to-biome", () => {
12197
).toMatchSnapshot();
12298

12399
expect(existsSync(eslintConfigFilePath)).toBe(false);
124-
expect(readFileSync(preCommitHookPath, "utf-8")).toMatchSnapshot();
125100
});
126101

127102
it("should keep custom eslint config files", async () => {
@@ -484,4 +459,16 @@ export default tsDevToolsCore;
484459
);
485460
});
486461
});
462+
463+
describe("hooks", () => {
464+
it("should export the managed pre-commit hook", () => {
465+
expect(hooks).toEqual([
466+
{
467+
name: "pre-commit",
468+
command:
469+
"npx --no-install biome check --error-on-warnings --staged --write",
470+
},
471+
]);
472+
});
473+
});
487474
});

packages/core/src/install/migrations/20260311120000-migrate-to-biome.ts

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { dirname, join, relative } from "node:path";
33

44
import { CmdService } from "../../services/CmdService";
55
import { FileService } from "../../services/FileService";
6-
import { GitService } from "../../services/GitService";
6+
import type { ManagedGitHook } from "../../services/HooksService";
77
import type { MigrationUpFunction } from "../../services/MigrationsService";
88
import { PackageJson } from "../../services/PackageJson";
99

@@ -12,11 +12,16 @@ const ESLINT_CONFIG_FILE_NAME = "eslint.config.mjs";
1212
const PRE_COMMIT_HOOK_NAME = "pre-commit";
1313
const BIOME_INIT_COMMAND = "npx @biomejs/biome init";
1414

15-
const OLD_PRE_COMMIT_COMMAND =
16-
"npx --no-install lint-staged && npx --no-install pretty-quick --staged";
1715
const NEW_PRE_COMMIT_COMMAND =
1816
"npx --no-install biome check --error-on-warnings --staged --write";
1917

18+
export const hooks: ManagedGitHook[] = [
19+
{
20+
name: PRE_COMMIT_HOOK_NAME,
21+
command: NEW_PRE_COMMIT_COMMAND,
22+
},
23+
];
24+
2025
const MANAGED_ESLINT_MARKERS = [
2126
"tsDevToolsCore",
2227
"tsDevToolsReact",
@@ -83,7 +88,6 @@ export const up: MigrationUpFunction = async (
8388
enableBiomeVcsIntegration(absoluteProjectDir);
8489
migrateCommonViteStarterFiles(absoluteProjectDir);
8590
deleteManagedEslintConfig(absoluteProjectDir, hasManagedEslintConfigFile);
86-
updateManagedPreCommitHook(absoluteProjectDir);
8791
};
8892

8993
async function runBiomeInit(absoluteProjectDir: string): Promise<void> {
@@ -149,7 +153,7 @@ function getBiomeVcsRoot(absoluteProjectDir: string): string | undefined {
149153
return undefined;
150154
}
151155

152-
return relative(absoluteProjectDir, vcsRootPath) || undefined;
156+
return relative(absoluteProjectDir, vcsRootPath);
153157
}
154158

155159
function getEnclosingVcsRootPath(
@@ -293,12 +297,3 @@ function deleteManagedEslintConfig(
293297

294298
unlinkSync(eslintConfigFilePath);
295299
}
296-
297-
function updateManagedPreCommitHook(absoluteProjectDir: string): void {
298-
GitService.updateGitHook(
299-
absoluteProjectDir,
300-
PRE_COMMIT_HOOK_NAME,
301-
OLD_PRE_COMMIT_COMMAND,
302-
NEW_PRE_COMMIT_COMMAND,
303-
);
304-
}

packages/core/src/install/migrations/__snapshots__/20260311120000-migrate-to-biome.spec.ts.snap

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,6 @@ exports[`Migration 20260311120000-migrate-to-biome > Up > should migrate ts-dev-
5050
"
5151
`;
5252

53-
exports[`Migration 20260311120000-migrate-to-biome > Up > should migrate ts-dev-tools eslint and prettier setup to biome 3`] = `
54-
"#!/bin/sh
55-
56-
# Created by ts-dev-tools (https://escemi-tech.github.io/ts-dev-tools/)
57-
58-
npx --no-install biome check --error-on-warnings --staged --write"
59-
`;
60-
6153
exports[`Migration 20260311120000-migrate-to-biome > Up > should not overwrite an existing biome config 1`] = `
6254
"{
6355
"vcs": {
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
2+
import { join } from "node:path";
3+
4+
import {
5+
createProjectForTestFile,
6+
deleteTestProject,
7+
} from "../tests/test-project";
8+
import { GitService } from "./GitService";
9+
import { HooksService } from "./HooksService";
10+
11+
// Set to false to avoid using the cache
12+
const useCache = true;
13+
// Set to false to inspect the test project directory after the test
14+
const shouldCleanupAfterTest = true;
15+
16+
describe("HooksService", () => {
17+
let testProjectDir: string;
18+
19+
beforeAll(() => {
20+
if (!useCache) {
21+
console.warn("Cache is disabled. Enable it one dev is done.");
22+
}
23+
if (!shouldCleanupAfterTest) {
24+
console.warn("Cleanup is disabled. Enable it one dev is done.");
25+
}
26+
});
27+
28+
beforeEach(async () => {
29+
testProjectDir = await createProjectForTestFile(__filename, useCache);
30+
});
31+
32+
afterEach(async () => {
33+
if (shouldCleanupAfterTest) {
34+
await deleteTestProject(__filename);
35+
}
36+
});
37+
38+
describe("consolidateManagedGitHooks", () => {
39+
it("should keep the previous managed command as legacy when a hook changes", () => {
40+
const managedGitHooks = new Map();
41+
42+
HooksService.consolidateManagedGitHooks(
43+
testProjectDir,
44+
[{ name: "pre-commit", command: "echo old;" }],
45+
managedGitHooks,
46+
);
47+
HooksService.consolidateManagedGitHooks(
48+
testProjectDir,
49+
[{ name: "pre-commit", command: "echo new;" }],
50+
managedGitHooks,
51+
);
52+
53+
expect(Array.from(managedGitHooks.values())).toEqual([
54+
{
55+
name: "pre-commit",
56+
command: "echo new;",
57+
legacyCommands: ["echo old;"],
58+
},
59+
]);
60+
});
61+
});
62+
63+
describe("applyManagedGitHooks", () => {
64+
it("should update a managed hook from a legacy command", async () => {
65+
vi.spyOn(GitService, "isGitRepository").mockResolvedValue(true);
66+
67+
const gitHookFilePath = join(
68+
testProjectDir,
69+
".git",
70+
"hooks",
71+
"pre-commit",
72+
);
73+
writeFileSync(
74+
gitHookFilePath,
75+
GitService.GIT_HOOK_TEMPLATE.replace("%gitHookCommand%", "echo old;"),
76+
);
77+
78+
await HooksService.applyManagedGitHooks(testProjectDir, [
79+
{
80+
name: "pre-commit",
81+
command: "echo new;",
82+
legacyCommands: ["echo old;"],
83+
},
84+
]);
85+
86+
expect(existsSync(gitHookFilePath)).toBe(true);
87+
expect(readFileSync(gitHookFilePath, "utf-8")).toBe(`#!/bin/sh
88+
89+
# Created by ts-dev-tools (https://escemi-tech.github.io/ts-dev-tools/)
90+
91+
echo new;`);
92+
});
93+
});
94+
});

0 commit comments

Comments
 (0)