Skip to content

Commit 9287db0

Browse files
refactor(cli): collapse 5 install-X-hook functions behind one strategy
install-git-hook-config-managers.ts had five near-identical functions (installSimpleGitHooks, installGhooks, installPreCommitNpm, installPrettyQuick, installPackageJsonPreCommitString) that each: 1. Read package.json 2. Walked to or created a specific nested config object 3. Appended the react-doctor command to a leaf value (newline-joined string OR array of strings) 4. Wrote the file back The differences were the path ("simple-git-hooks" vs "config.ghooks" vs "pre-commit" vs "gitHooks"...) and the leaf shape (string vs array). 100+ lines of copy-pasted boilerplate per manager. Collapse to one installPackageJsonHook(options, { kind, path, leafShape }) strategy. Each manager becomes a 5-line config: export const installSimpleGitHooks = (options) => installPackageJsonHook(options, { kind: GitHookKind.SimpleGitHooks, path: ["simple-git-hooks", "pre-commit"], leafShape: "string", }); Yorkie + GitHooksJs were previously special-cased in install-git-hook.ts because they reused installPackageJsonPreCommitString as a parameterized helper. They now get their own one-liner entry points, which makes the dispatch table flat (every kind -> its own installer) and removes the generic helper. Lefthook / pre-commit / overcommit stay separate because they write text/YAML files, not JSON. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
1 parent dacde53 commit 9287db0

2 files changed

Lines changed: 81 additions & 70 deletions

File tree

packages/react-doctor/src/cli/utils/install-git-hook-config-managers.ts

Lines changed: 77 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -16,30 +16,6 @@ import {
1616
} from "./git-hook-types.js";
1717
import { removeLegacyManagedRunner } from "./install-git-hook-file.js";
1818

19-
export const installSimpleGitHooks = (options: InstallGitHookOptions): InstallGitHookResult => {
20-
const packageJsonPath = getPackageJsonPath(options.projectRoot);
21-
const didHookExist = existsSync(packageJsonPath);
22-
const packageJson = readPackageJson(options.projectRoot);
23-
const nextPackageJson = isRecord(packageJson) ? { ...packageJson } : {};
24-
const existingConfig = nextPackageJson["simple-git-hooks"];
25-
const nextConfig = isRecord(existingConfig) ? { ...existingConfig } : {};
26-
const existingPreCommit =
27-
typeof nextConfig["pre-commit"] === "string" ? nextConfig["pre-commit"] : "";
28-
const nextPreCommit = existingPreCommit.includes(REACT_DOCTOR_COMMAND)
29-
? existingPreCommit
30-
: [existingPreCommit, NON_BLOCKING_REACT_DOCTOR_COMMAND].filter(Boolean).join("\n");
31-
nextConfig["pre-commit"] = nextPreCommit;
32-
nextPackageJson["simple-git-hooks"] = nextConfig;
33-
writeJsonFile(packageJsonPath, nextPackageJson);
34-
removeLegacyManagedRunner(options.projectRoot);
35-
36-
return {
37-
hookPath: packageJsonPath,
38-
kind: GitHookKind.SimpleGitHooks,
39-
status: didHookExist ? "updated" : "created",
40-
};
41-
};
42-
4319
const appendStringCommand = (existingCommand: unknown): string => {
4420
const existingCommandText =
4521
typeof existingCommand === "string"
@@ -63,66 +39,102 @@ const appendArrayCommand = (existingCommands: unknown): string[] => {
6339
: [...commands, NON_BLOCKING_REACT_DOCTOR_COMMAND];
6440
};
6541

66-
export const installPackageJsonPreCommitString = (
42+
interface PackageJsonHookStrategy {
43+
readonly kind: GitHookKind;
44+
/**
45+
* Dotted path of keys into `package.json` to reach the leaf where
46+
* the pre-commit command lives. `["simple-git-hooks", "pre-commit"]`
47+
* walks `package.json["simple-git-hooks"]["pre-commit"]`. Intermediate
48+
* objects are created when missing; non-record intermediates are
49+
* replaced with a fresh empty object.
50+
*/
51+
readonly path: ReadonlyArray<string>;
52+
/**
53+
* Shape of the leaf value the manager expects. `"string"` joins
54+
* commands with newlines (the shape simple-git-hooks / ghooks /
55+
* yorkie / pretty-quick / git-hooks-js use); `"array"` keeps each
56+
* command as a separate string element (the shape pre-commit-npm
57+
* uses).
58+
*/
59+
readonly leafShape: "string" | "array";
60+
}
61+
62+
const installPackageJsonHook = (
6763
options: InstallGitHookOptions,
68-
kind: GitHookKind,
69-
configKey: string,
64+
strategy: PackageJsonHookStrategy,
7065
): InstallGitHookResult => {
7166
const packageJsonPath = getPackageJsonPath(options.projectRoot);
7267
const didHookExist = existsSync(packageJsonPath);
7368
const packageJson = readPackageJson(options.projectRoot);
7469
const nextPackageJson = isRecord(packageJson) ? { ...packageJson } : {};
75-
const existingConfig = nextPackageJson[configKey];
76-
const nextConfig = isRecord(existingConfig) ? { ...existingConfig } : {};
77-
nextConfig["pre-commit"] = appendStringCommand(nextConfig["pre-commit"]);
78-
nextPackageJson[configKey] = nextConfig;
70+
71+
// Walk down to the parent of the leaf, cloning each intermediate
72+
// record so the original package.json shape isn't mutated in place
73+
// (writeJsonFile re-serializes the new tree).
74+
const parentKeys = strategy.path.slice(0, -1);
75+
const leafKey = strategy.path[strategy.path.length - 1];
76+
let parent: Record<string, unknown> = nextPackageJson;
77+
for (const key of parentKeys) {
78+
const existing = parent[key];
79+
const cloned = isRecord(existing) ? { ...existing } : {};
80+
parent[key] = cloned;
81+
parent = cloned;
82+
}
83+
parent[leafKey] =
84+
strategy.leafShape === "array"
85+
? appendArrayCommand(parent[leafKey])
86+
: appendStringCommand(parent[leafKey]);
87+
7988
writeJsonFile(packageJsonPath, nextPackageJson);
8089
removeLegacyManagedRunner(options.projectRoot);
8190
return {
8291
hookPath: packageJsonPath,
83-
kind,
92+
kind: strategy.kind,
8493
status: didHookExist ? "updated" : "created",
8594
};
8695
};
8796

88-
export const installGhooks = (options: InstallGitHookOptions): InstallGitHookResult => {
89-
const packageJsonPath = getPackageJsonPath(options.projectRoot);
90-
const didHookExist = existsSync(packageJsonPath);
91-
const packageJson = readPackageJson(options.projectRoot);
92-
const nextPackageJson = isRecord(packageJson) ? { ...packageJson } : {};
93-
const existingConfig = nextPackageJson.config;
94-
const nextConfig = isRecord(existingConfig) ? { ...existingConfig } : {};
95-
const existingGhooks = nextConfig.ghooks;
96-
const nextGhooks = isRecord(existingGhooks) ? { ...existingGhooks } : {};
97-
nextGhooks["pre-commit"] = appendStringCommand(nextGhooks["pre-commit"]);
98-
nextConfig.ghooks = nextGhooks;
99-
nextPackageJson.config = nextConfig;
100-
writeJsonFile(packageJsonPath, nextPackageJson);
101-
removeLegacyManagedRunner(options.projectRoot);
102-
return {
103-
hookPath: packageJsonPath,
97+
export const installSimpleGitHooks = (options: InstallGitHookOptions): InstallGitHookResult =>
98+
installPackageJsonHook(options, {
99+
kind: GitHookKind.SimpleGitHooks,
100+
path: ["simple-git-hooks", "pre-commit"],
101+
leafShape: "string",
102+
});
103+
104+
export const installGhooks = (options: InstallGitHookOptions): InstallGitHookResult =>
105+
installPackageJsonHook(options, {
104106
kind: GitHookKind.Ghooks,
105-
status: didHookExist ? "updated" : "created",
106-
};
107-
};
107+
path: ["config", "ghooks", "pre-commit"],
108+
leafShape: "string",
109+
});
108110

109-
export const installPreCommitNpm = (options: InstallGitHookOptions): InstallGitHookResult => {
110-
const packageJsonPath = getPackageJsonPath(options.projectRoot);
111-
const didHookExist = existsSync(packageJsonPath);
112-
const packageJson = readPackageJson(options.projectRoot);
113-
const nextPackageJson = isRecord(packageJson) ? { ...packageJson } : {};
114-
nextPackageJson["pre-commit"] = appendArrayCommand(nextPackageJson["pre-commit"]);
115-
writeJsonFile(packageJsonPath, nextPackageJson);
116-
removeLegacyManagedRunner(options.projectRoot);
117-
return {
118-
hookPath: packageJsonPath,
111+
export const installPreCommitNpm = (options: InstallGitHookOptions): InstallGitHookResult =>
112+
installPackageJsonHook(options, {
119113
kind: GitHookKind.PreCommitNpm,
120-
status: didHookExist ? "updated" : "created",
121-
};
122-
};
114+
path: ["pre-commit"],
115+
leafShape: "array",
116+
});
123117

124118
export const installPrettyQuick = (options: InstallGitHookOptions): InstallGitHookResult =>
125-
installPackageJsonPreCommitString(options, GitHookKind.PrettyQuick, "gitHooks");
119+
installPackageJsonHook(options, {
120+
kind: GitHookKind.PrettyQuick,
121+
path: ["gitHooks", "pre-commit"],
122+
leafShape: "string",
123+
});
124+
125+
export const installYorkie = (options: InstallGitHookOptions): InstallGitHookResult =>
126+
installPackageJsonHook(options, {
127+
kind: GitHookKind.Yorkie,
128+
path: ["gitHooks", "pre-commit"],
129+
leafShape: "string",
130+
});
131+
132+
export const installGitHooksJs = (options: InstallGitHookOptions): InstallGitHookResult =>
133+
installPackageJsonHook(options, {
134+
kind: GitHookKind.GitHooksJs,
135+
path: ["git-hooks", "pre-commit"],
136+
leafShape: "string",
137+
});
126138

127139
const appendIndentedBlockToTopLevelSection = (
128140
content: string,

packages/react-doctor/src/cli/utils/install-git-hook.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,14 @@ import {
2727
} from "./git-hook-types.js";
2828
import {
2929
installGhooks,
30+
installGitHooksJs,
3031
installLefthook,
3132
installOvercommit,
32-
installPackageJsonPreCommitString,
3333
installPreCommit,
3434
installPreCommitNpm,
3535
installPrettyQuick,
3636
installSimpleGitHooks,
37+
installYorkie,
3738
} from "./install-git-hook-config-managers.js";
3839
import { installDirectGitHook } from "./install-git-hook-file.js";
3940

@@ -213,11 +214,9 @@ export const installReactDoctorGitHook = (options: InstallGitHookOptions): Insta
213214
if (options.kind === GitHookKind.Lefthook) return installLefthook(options);
214215
if (options.kind === GitHookKind.PreCommit) return installPreCommit(options);
215216
if (options.kind === GitHookKind.Overcommit) return installOvercommit(options);
216-
if (options.kind === GitHookKind.Yorkie)
217-
return installPackageJsonPreCommitString(options, GitHookKind.Yorkie, "gitHooks");
217+
if (options.kind === GitHookKind.Yorkie) return installYorkie(options);
218218
if (options.kind === GitHookKind.Ghooks) return installGhooks(options);
219-
if (options.kind === GitHookKind.GitHooksJs)
220-
return installPackageJsonPreCommitString(options, GitHookKind.GitHooksJs, "git-hooks");
219+
if (options.kind === GitHookKind.GitHooksJs) return installGitHooksJs(options);
221220
if (options.kind === GitHookKind.PreCommitNpm) return installPreCommitNpm(options);
222221
if (options.kind === GitHookKind.PrettyQuick) return installPrettyQuick(options);
223222
return installDirectGitHook(options);

0 commit comments

Comments
 (0)