Skip to content

Commit 4e048d1

Browse files
authored
fix(typegen): preserve fmodata customizations with clearOldFiles (#122)
<!-- CURSOR_SUMMARY --> > [!NOTE] > **Medium Risk** > Changes the `clearOldFiles` behavior to delete files *after* regeneration via a recursive cleanup, which could remove unexpected entries if the output path is misconfigured, but is otherwise localized to type generation output. > > **Overview** > Fixes `fmodata` type generation so `clearOldFiles` no longer nukes the output directory before merging with existing files; existing schemas are now always parsed so field-level customizations (e.g. validators) are preserved. > > When `clearOldFiles` is enabled, stale files/directories are now removed *after* generation by tracking regenerated files (including `index.ts`) and deleting any other entries in the output tree. Adds an e2e test covering customization preservation plus stale file cleanup, and includes a patch changeset. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit ea7f4c2. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 944925b commit 4e048d1

File tree

3 files changed

+85
-6
lines changed

3 files changed

+85
-6
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@proofkit/typegen": patch
3+
---
4+
5+
Fix fmodata type generation to preserve existing field-level customizations even when `clearOldFiles` is enabled.
6+
7+
Stale files in the output directory are now removed after regeneration, so dead generated files are still cleaned up without discarding validator customizations from existing schemas.

packages/typegen/src/fmodata/generateODataTypes.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,24 +1055,20 @@ export async function generateODataTypes(
10551055
const resolvedOutputPath = resolve(cwd, outputPath);
10561056
await mkdir(resolvedOutputPath, { recursive: true });
10571057

1058-
if (clearOldFiles) {
1059-
// Clear the directory if requested (but keep the directory itself)
1060-
fs.emptyDirSync(resolvedOutputPath);
1061-
}
1062-
10631058
// Create ts-morph project for file manipulation
10641059
const project = new Project({});
10651060

10661061
// Generate one file per table occurrence
10671062
const exportStatements: string[] = [];
1063+
const keptFiles = new Set<string>();
10681064

10691065
for (const generated of generatedTOs) {
10701066
const fileName = `${sanitizeFileName(generated.varName)}.ts`;
10711067
const filePath = join(resolvedOutputPath, fileName);
10721068

10731069
// Check if file exists and parse it
10741070
let existingFields: ParsedTableOccurrence | undefined;
1075-
if (fs.existsSync(filePath) && !clearOldFiles) {
1071+
if (fs.existsSync(filePath)) {
10761072
try {
10771073
const existingSourceFile = project.addSourceFileAtPath(filePath);
10781074
const parsed = parseExistingTableFile(existingSourceFile);
@@ -1389,6 +1385,7 @@ export async function generateODataTypes(
13891385
project.createSourceFile(filePath, fileContent, {
13901386
overwrite: true,
13911387
});
1388+
keptFiles.add(filePath);
13921389

13931390
// Collect export statement for index file
13941391
exportStatements.push(`export { ${regenerated.varName} } from "./${sanitizeFileName(regenerated.varName)}";`);
@@ -1406,7 +1403,32 @@ ${exportStatements.join("\n")}
14061403
project.createSourceFile(indexPath, indexContent, {
14071404
overwrite: true,
14081405
});
1406+
keptFiles.add(indexPath);
14091407

14101408
// Format and save files
14111409
await formatAndSaveSourceFiles(project, cwd);
1410+
1411+
if (clearOldFiles) {
1412+
// For fmodata generation, preserve customizations by merging first,
1413+
// then remove files/directories that were not regenerated this run.
1414+
const keepSet = new Set(Array.from(keptFiles).map((p) => resolve(p)));
1415+
const deleteStaleEntries = (dirPath: string): void => {
1416+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
1417+
for (const entry of entries) {
1418+
const entryPath = resolve(dirPath, entry.name);
1419+
if (entry.isDirectory()) {
1420+
deleteStaleEntries(entryPath);
1421+
const remainingEntries = fs.readdirSync(entryPath);
1422+
if (remainingEntries.length === 0 && !keepSet.has(entryPath)) {
1423+
fs.removeSync(entryPath);
1424+
}
1425+
continue;
1426+
}
1427+
if (!keepSet.has(entryPath)) {
1428+
fs.removeSync(entryPath);
1429+
}
1430+
}
1431+
};
1432+
deleteStaleEntries(resolvedOutputPath);
1433+
}
14121434
}

packages/typegen/tests/e2e/fmodata-preserve-customizations.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,4 +270,54 @@ describe("fmodata generateODataTypes preserves user customizations", () => {
270270
await fs.rm(tmpDir, { recursive: true, force: true });
271271
}
272272
});
273+
274+
it("preserves custom validators and removes stale files when clearOldFiles is true", async () => {
275+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-fmodata-preserve-"));
276+
277+
try {
278+
const entitySetName = "MyTable";
279+
const entityTypeName = "NS.MyTable";
280+
const metadata = makeMetadata({
281+
entitySetName,
282+
entityTypeName,
283+
fields: [{ name: "FieldA", type: "Edm.String", fieldId: "F1" }],
284+
});
285+
286+
const existingFilePath = path.join(tmpDir, "MyTable.ts");
287+
await fs.writeFile(
288+
existingFilePath,
289+
[
290+
`import { fmTableOccurrence, textField } from "@proofkit/fmodata";`,
291+
`import { z } from "zod/v4";`,
292+
"",
293+
`export const MyTable = fmTableOccurrence("MyTable", {`,
294+
` "FieldA": textField().inputValidator(z.string()).entityId("F1"),`,
295+
"});",
296+
"",
297+
].join("\n"),
298+
"utf8",
299+
);
300+
301+
const staleFilePath = path.join(tmpDir, "OldTable.ts");
302+
await fs.writeFile(staleFilePath, `export const OldTable = "stale";\n`, "utf8");
303+
304+
await generateODataTypes(metadata, {
305+
type: "fmodata",
306+
path: tmpDir,
307+
clearOldFiles: true,
308+
tables: [{ tableName: "MyTable" }],
309+
});
310+
311+
const regenerated = await fs.readFile(existingFilePath, "utf8");
312+
expect(regenerated).toContain(`FieldA: textField().entityId("F1").inputValidator(z.string())`);
313+
314+
const staleExists = await fs
315+
.access(staleFilePath)
316+
.then(() => true)
317+
.catch(() => false);
318+
expect(staleExists).toBe(false);
319+
} finally {
320+
await fs.rm(tmpDir, { recursive: true, force: true });
321+
}
322+
});
273323
});

0 commit comments

Comments
 (0)