Skip to content

Commit 52d4361

Browse files
committed
fix: replace shell exec with execFile in cleanupFiles
cleanupFiles used the shell-based exec() helper for rm/mkdir/cp commands that don't require shell features. Switched all calls to execFile() with explicit args arrays, eliminating unnecessary shell parsing and aligning with the repo convention that exec() is reserved for shell-only features.
1 parent cc34435 commit 52d4361

3 files changed

Lines changed: 36 additions & 30 deletions

File tree

architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ Plain async functions with no UI dependencies. Each operation receives explicit
8585
| `cloneRepo(projectName)` | Shallow clone, checkout latest tag, rm .git, git init. Uses `execFile` (no shell) for all commands except `git checkout $(...)` which needs shell substitution. |
8686
| `createEnvFile(projectFolder)` | Copy .env.example to .env.local |
8787
| `installPackages(projectFolder, mode, features)` | Full: `pnpm i`. Custom: `pnpm remove` deselected packages + postinstall. Uses `execFile` exclusively (no shell). |
88-
| `cleanupFiles(projectFolder, mode, features)` | Remove files/folders for deselected features, patch package.json scripts, remove .install-files |
88+
| `cleanupFiles(projectFolder, mode, features)` | Remove files/folders for deselected features, patch package.json scripts, remove .install-files. Uses `execFile` exclusively (no shell). |
8989

9090
### Shell Execution (`source/operations/exec.ts`)
9191

source/__tests__/operations/cleanupFiles.test.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@ vi.mock('node:fs', () => ({
2424
writeFileSync: vi.fn(),
2525
}))
2626

27-
const { exec } = await import('../../operations/exec.js')
27+
const { execFile } = await import('../../operations/exec.js')
2828
const { readFileSync, writeFileSync } = await import('node:fs')
2929
const { cleanupFiles } = await import('../../operations/cleanupFiles.js')
3030

31-
function getExecCommands(): string[] {
32-
return vi.mocked(exec).mock.calls.map((call) => call[0] as string)
31+
function getExecFileCommands(): string[] {
32+
return vi
33+
.mocked(execFile)
34+
.mock.calls.map((call) => `${call[0]} ${(call[1] as string[]).join(' ')}`)
3335
}
3436

3537
function getWrittenPackageJson(): Record<string, unknown> {
@@ -63,7 +65,7 @@ describe('cleanupFiles', () => {
6365
it('only removes .install-files', async () => {
6466
await cleanupFiles('/project/my_app', 'full')
6567

66-
const commands = getExecCommands()
68+
const commands = getExecFileCommands()
6769
expect(commands).toHaveLength(1)
6870
expect(commands[0]).toBe('rm -rf .install-files')
6971
})
@@ -80,7 +82,7 @@ describe('cleanupFiles', () => {
8082
const allFeatures: FeatureName[] = ['demo', 'subgraph', 'typedoc', 'vocs', 'husky']
8183
await cleanupFiles('/project/my_app', 'custom', allFeatures)
8284

83-
const commands = getExecCommands()
85+
const commands = getExecFileCommands()
8486
expect(commands).toEqual(['rm -rf .install-files'])
8587
expect(writeFileSync).toHaveBeenCalled()
8688
})
@@ -102,7 +104,7 @@ describe('cleanupFiles', () => {
102104
it('removes home folder, recreates it, copies replacement', async () => {
103105
await cleanupFiles('/project/my_app', 'custom', ['subgraph', 'typedoc', 'vocs', 'husky'])
104106

105-
const commands = getExecCommands()
107+
const commands = getExecFileCommands()
106108
expect(commands).toContain('rm -rf src/components/pageComponents/home')
107109
expect(commands).toContain('mkdir -p src/components/pageComponents/home')
108110
expect(commands).toContain(
@@ -115,14 +117,14 @@ describe('cleanupFiles', () => {
115117
it('removes src/subgraphs', async () => {
116118
await cleanupFiles('/project/my_app', 'custom', ['demo', 'typedoc', 'vocs', 'husky'])
117119

118-
const commands = getExecCommands()
120+
const commands = getExecFileCommands()
119121
expect(commands).toContain('rm -rf src/subgraphs')
120122
})
121123

122124
it('cleans up subgraph demos when demo IS selected', async () => {
123125
await cleanupFiles('/project/my_app', 'custom', ['demo', 'typedoc', 'vocs', 'husky'])
124126

125-
const commands = getExecCommands()
127+
const commands = getExecFileCommands()
126128
const homeFolder = 'src/components/pageComponents/home'
127129
expect(commands).toContain(`rm -rf ${homeFolder}/Examples/demos/subgraphs`)
128130
expect(commands).toContain(`rm ${homeFolder}/Examples/index.tsx`)
@@ -134,7 +136,7 @@ describe('cleanupFiles', () => {
134136
it('does NOT clean up subgraph demos when demo is also deselected', async () => {
135137
await cleanupFiles('/project/my_app', 'custom', ['typedoc', 'vocs', 'husky'])
136138

137-
const commands = getExecCommands()
139+
const commands = getExecFileCommands()
138140
const demoCleanupCommands = commands.filter((cmd) => cmd.includes('Examples/demos/subgraphs'))
139141
expect(demoCleanupCommands).toHaveLength(0)
140142
})
@@ -152,7 +154,7 @@ describe('cleanupFiles', () => {
152154
it('removes typedoc.json', async () => {
153155
await cleanupFiles('/project/my_app', 'custom', ['demo', 'subgraph', 'vocs', 'husky'])
154156

155-
const commands = getExecCommands()
157+
const commands = getExecFileCommands()
156158
expect(commands).toContain('rm typedoc.json')
157159
})
158160

@@ -169,7 +171,7 @@ describe('cleanupFiles', () => {
169171
it('removes vocs.config.ts and docs folder', async () => {
170172
await cleanupFiles('/project/my_app', 'custom', ['demo', 'subgraph', 'typedoc', 'husky'])
171173

172-
const commands = getExecCommands()
174+
const commands = getExecFileCommands()
173175
expect(commands).toContain('rm vocs.config.ts')
174176
expect(commands).toContain('rm -rf docs')
175177
})
@@ -189,7 +191,7 @@ describe('cleanupFiles', () => {
189191
it('removes husky folder and config files', async () => {
190192
await cleanupFiles('/project/my_app', 'custom', ['demo', 'subgraph', 'typedoc', 'vocs'])
191193

192-
const commands = getExecCommands()
194+
const commands = getExecFileCommands()
193195
expect(commands).toContain('rm -rf .husky')
194196
expect(commands).toContain('rm .lintstagedrc.mjs')
195197
expect(commands).toContain('rm commitlint.config.js')
@@ -208,7 +210,7 @@ describe('cleanupFiles', () => {
208210
it('runs all cleanup operations', async () => {
209211
await cleanupFiles('/project/my_app', 'custom', [])
210212

211-
const commands = getExecCommands()
213+
const commands = getExecFileCommands()
212214
expect(commands).toContain('rm -rf src/components/pageComponents/home')
213215
expect(commands).toContain('rm -rf src/subgraphs')
214216
expect(commands).toContain('rm typedoc.json')
@@ -237,7 +239,7 @@ describe('cleanupFiles', () => {
237239
it('always removes .install-files as the last step', async () => {
238240
await cleanupFiles('/project/my_app', 'custom', ['demo'])
239241

240-
const commands = getExecCommands()
242+
const commands = getExecFileCommands()
241243
expect(commands.at(-1)).toBe('rm -rf .install-files')
242244
})
243245
})

source/operations/cleanupFiles.ts

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { resolve } from 'node:path'
33
import type { FeatureName } from '../constants/config.js'
44
import type { InstallationType } from '../types/types.js'
55
import { isFeatureSelected } from '../utils/utils.js'
6-
import { exec } from './exec.js'
6+
import { execFile } from './exec.js'
77

88
function patchPackageJson(projectFolder: string, features: FeatureName[]): void {
99
const packageJsonPath = resolve(projectFolder, 'package.json')
@@ -31,40 +31,44 @@ function patchPackageJson(projectFolder: string, features: FeatureName[]): void
3131
}
3232

3333
async function cleanupDemo(projectFolder: string): Promise<void> {
34-
await exec('rm -rf src/components/pageComponents/home', { cwd: projectFolder })
35-
await exec('mkdir -p src/components/pageComponents/home', { cwd: projectFolder })
36-
await exec('cp .install-files/home/index.tsx src/components/pageComponents/home/', {
34+
await execFile('rm', ['-rf', 'src/components/pageComponents/home'], { cwd: projectFolder })
35+
await execFile('mkdir', ['-p', 'src/components/pageComponents/home'], { cwd: projectFolder })
36+
await execFile('cp', ['.install-files/home/index.tsx', 'src/components/pageComponents/home/'], {
3737
cwd: projectFolder,
3838
})
3939
}
4040

4141
async function cleanupSubgraph(projectFolder: string, features: FeatureName[]): Promise<void> {
42-
await exec('rm -rf src/subgraphs', { cwd: projectFolder })
42+
await execFile('rm', ['-rf', 'src/subgraphs'], { cwd: projectFolder })
4343

4444
if (isFeatureSelected('demo', features)) {
4545
const homeFolder = 'src/components/pageComponents/home'
4646

47-
await exec(`rm -rf ${homeFolder}/Examples/demos/subgraphs`, { cwd: projectFolder })
48-
await exec(`rm ${homeFolder}/Examples/index.tsx`, { cwd: projectFolder })
49-
await exec(`cp .install-files/home/Examples/index.tsx ${homeFolder}/Examples/index.tsx`, {
47+
await execFile('rm', ['-rf', `${homeFolder}/Examples/demos/subgraphs`], {
5048
cwd: projectFolder,
5149
})
50+
await execFile('rm', [`${homeFolder}/Examples/index.tsx`], { cwd: projectFolder })
51+
await execFile(
52+
'cp',
53+
['.install-files/home/Examples/index.tsx', `${homeFolder}/Examples/index.tsx`],
54+
{ cwd: projectFolder },
55+
)
5256
}
5357
}
5458

5559
async function cleanupTypedoc(projectFolder: string): Promise<void> {
56-
await exec('rm typedoc.json', { cwd: projectFolder })
60+
await execFile('rm', ['typedoc.json'], { cwd: projectFolder })
5761
}
5862

5963
async function cleanupVocs(projectFolder: string): Promise<void> {
60-
await exec('rm vocs.config.ts', { cwd: projectFolder })
61-
await exec('rm -rf docs', { cwd: projectFolder })
64+
await execFile('rm', ['vocs.config.ts'], { cwd: projectFolder })
65+
await execFile('rm', ['-rf', 'docs'], { cwd: projectFolder })
6266
}
6367

6468
async function cleanupHusky(projectFolder: string): Promise<void> {
65-
await exec('rm -rf .husky', { cwd: projectFolder })
66-
await exec('rm .lintstagedrc.mjs', { cwd: projectFolder })
67-
await exec('rm commitlint.config.js', { cwd: projectFolder })
69+
await execFile('rm', ['-rf', '.husky'], { cwd: projectFolder })
70+
await execFile('rm', ['.lintstagedrc.mjs'], { cwd: projectFolder })
71+
await execFile('rm', ['commitlint.config.js'], { cwd: projectFolder })
6872
}
6973

7074
export async function cleanupFiles(
@@ -96,5 +100,5 @@ export async function cleanupFiles(
96100
patchPackageJson(projectFolder, features)
97101
}
98102

99-
await exec('rm -rf .install-files', { cwd: projectFolder })
103+
await execFile('rm', ['-rf', '.install-files'], { cwd: projectFolder })
100104
}

0 commit comments

Comments
 (0)