Skip to content

Commit bebf7a4

Browse files
committed
add impl and test
1 parent 5576d9a commit bebf7a4

File tree

2 files changed

+435
-0
lines changed

2 files changed

+435
-0
lines changed

src/utils/gitignore.ts

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import {readFile, appendFile, writeFile, access} from 'node:fs/promises';
2+
import path from 'path';
3+
4+
export interface GitignoreUpdateOptions {
5+
/**
6+
* The base directory where .gitignore is located (defaults to process.cwd())
7+
*/
8+
baseDirectory?: string;
9+
/**
10+
* Custom comment header to add before the gitignore entries
11+
*/
12+
commentHeader?: string;
13+
}
14+
15+
export interface GitignoreUpdateResult {
16+
/**
17+
* Whether the operation was successful
18+
*/
19+
success: boolean;
20+
/**
21+
* The action that was performed
22+
*/
23+
action: 'created' | 'updated' | 'skipped' | 'error';
24+
/**
25+
* A message describing what happened
26+
*/
27+
message: string;
28+
/**
29+
* The paths that were added (if any)
30+
*/
31+
addedPaths?: string[];
32+
/**
33+
* Error details if the operation failed
34+
*/
35+
error?: string;
36+
}
37+
38+
/**
39+
* Updates or creates a .gitignore file with the specified output paths
40+
*
41+
* @param outputPaths - Array of paths to add to .gitignore
42+
* @param options - Optional configuration for the gitignore update
43+
* @returns Result object with operation details
44+
*
45+
* @example
46+
* ```typescript
47+
* const result = await updateGitignore(['src/__gen__/payloads', 'src/__gen__/channels']);
48+
* if (result.success) {
49+
* console.log(result.message);
50+
* }
51+
* ```
52+
*/
53+
export async function updateGitignore(
54+
outputPaths: string[],
55+
options: GitignoreUpdateOptions = {}
56+
): Promise<GitignoreUpdateResult> {
57+
// eslint-disable-next-line no-undef
58+
const baseDirectory = options.baseDirectory || process.cwd();
59+
const commentHeader =
60+
options.commentHeader || '# The Codegen Project - generated files';
61+
const gitignorePath = path.join(baseDirectory, '.gitignore');
62+
63+
try {
64+
// Check if .gitignore exists
65+
await access(gitignorePath);
66+
67+
// Read existing content
68+
const existingContent = await readFile(gitignorePath, 'utf-8');
69+
70+
// Filter out paths that already exist in .gitignore
71+
const newPaths = outputPaths.filter(
72+
(outputPath) => !existingContent.includes(outputPath)
73+
);
74+
75+
if (newPaths.length === 0) {
76+
return {
77+
success: true,
78+
action: 'skipped',
79+
message:
80+
'All output directories already present in .gitignore, skipping',
81+
addedPaths: []
82+
};
83+
}
84+
85+
// Ensure file ends with newline before adding new entries
86+
const separator = existingContent.endsWith('\n') ? '' : '\n';
87+
const gitignoreEntry = `${separator}\n${commentHeader}\n${newPaths.join('\n')}\n`;
88+
89+
await appendFile(gitignorePath, gitignoreEntry);
90+
91+
return {
92+
success: true,
93+
action: 'updated',
94+
message: `Added ${newPaths.length} output director${newPaths.length === 1 ? 'y' : 'ies'} to .gitignore`,
95+
addedPaths: newPaths
96+
};
97+
} catch (error: any) {
98+
if (error.code === 'ENOENT') {
99+
// .gitignore doesn't exist, try to create it
100+
try {
101+
const gitignoreContent = `${commentHeader}\n${outputPaths.join('\n')}\n`;
102+
await writeFile(gitignorePath, gitignoreContent);
103+
104+
return {
105+
success: true,
106+
action: 'created',
107+
message: 'Created .gitignore with generated output directories',
108+
addedPaths: outputPaths
109+
};
110+
} catch (writeError: any) {
111+
// Failed to create .gitignore (e.g., directory doesn't exist)
112+
return {
113+
success: false,
114+
action: 'error',
115+
message: `Could not create .gitignore: ${writeError.message}`,
116+
error: writeError.message
117+
};
118+
}
119+
}
120+
121+
return {
122+
success: false,
123+
action: 'error',
124+
message: `Could not update .gitignore: ${error.message}`,
125+
error: error.message
126+
};
127+
}
128+
}
129+
130+
/**
131+
* Collects output paths from generator flags
132+
*
133+
* @param generatorConfigs - Map of generator names to their configurations
134+
* @returns Array of output paths
135+
*
136+
* @example
137+
* ```typescript
138+
* const paths = collectOutputPaths({
139+
* payloads: { outputPath: 'src/__gen__/payloads' },
140+
* channels: { outputPath: 'src/__gen__/channels' }
141+
* });
142+
* // Returns: ['src/__gen__/payloads', 'src/__gen__/channels']
143+
* ```
144+
*/
145+
export function collectOutputPaths(
146+
generatorConfigs: Record<string, {outputPath: string} | undefined>
147+
): string[] {
148+
const outputPaths: string[] = [];
149+
150+
for (const config of Object.values(generatorConfigs)) {
151+
if (config?.outputPath) {
152+
outputPaths.push(config.outputPath);
153+
}
154+
}
155+
156+
return outputPaths;
157+
}

0 commit comments

Comments
 (0)