@@ -16,30 +16,6 @@ import {
1616} from "./git-hook-types.js" ;
1717import { 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-
4319const 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
124118export 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
127139const appendIndentedBlockToTopLevelSection = (
128140 content : string ,
0 commit comments