diff --git a/.changeset/fix-fmodata-clearoldfiles-preserve-customizations.md b/.changeset/fix-fmodata-clearoldfiles-preserve-customizations.md new file mode 100644 index 0000000..4a07308 --- /dev/null +++ b/.changeset/fix-fmodata-clearoldfiles-preserve-customizations.md @@ -0,0 +1,7 @@ +--- +"@proofkit/typegen": patch +--- + +Fix fmodata type generation to preserve existing field-level customizations even when `clearOldFiles` is enabled. + +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. diff --git a/packages/typegen/src/fmodata/generateODataTypes.ts b/packages/typegen/src/fmodata/generateODataTypes.ts index 7e3edce..deaa063 100644 --- a/packages/typegen/src/fmodata/generateODataTypes.ts +++ b/packages/typegen/src/fmodata/generateODataTypes.ts @@ -1055,16 +1055,12 @@ export async function generateODataTypes( const resolvedOutputPath = resolve(cwd, outputPath); await mkdir(resolvedOutputPath, { recursive: true }); - if (clearOldFiles) { - // Clear the directory if requested (but keep the directory itself) - fs.emptyDirSync(resolvedOutputPath); - } - // Create ts-morph project for file manipulation const project = new Project({}); // Generate one file per table occurrence const exportStatements: string[] = []; + const keptFiles = new Set(); for (const generated of generatedTOs) { const fileName = `${sanitizeFileName(generated.varName)}.ts`; @@ -1072,7 +1068,7 @@ export async function generateODataTypes( // Check if file exists and parse it let existingFields: ParsedTableOccurrence | undefined; - if (fs.existsSync(filePath) && !clearOldFiles) { + if (fs.existsSync(filePath)) { try { const existingSourceFile = project.addSourceFileAtPath(filePath); const parsed = parseExistingTableFile(existingSourceFile); @@ -1389,6 +1385,7 @@ export async function generateODataTypes( project.createSourceFile(filePath, fileContent, { overwrite: true, }); + keptFiles.add(filePath); // Collect export statement for index file exportStatements.push(`export { ${regenerated.varName} } from "./${sanitizeFileName(regenerated.varName)}";`); @@ -1406,7 +1403,32 @@ ${exportStatements.join("\n")} project.createSourceFile(indexPath, indexContent, { overwrite: true, }); + keptFiles.add(indexPath); // Format and save files await formatAndSaveSourceFiles(project, cwd); + + if (clearOldFiles) { + // For fmodata generation, preserve customizations by merging first, + // then remove files/directories that were not regenerated this run. + const keepSet = new Set(Array.from(keptFiles).map((p) => resolve(p))); + const deleteStaleEntries = (dirPath: string): void => { + const entries = fs.readdirSync(dirPath, { withFileTypes: true }); + for (const entry of entries) { + const entryPath = resolve(dirPath, entry.name); + if (entry.isDirectory()) { + deleteStaleEntries(entryPath); + const remainingEntries = fs.readdirSync(entryPath); + if (remainingEntries.length === 0 && !keepSet.has(entryPath)) { + fs.removeSync(entryPath); + } + continue; + } + if (!keepSet.has(entryPath)) { + fs.removeSync(entryPath); + } + } + }; + deleteStaleEntries(resolvedOutputPath); + } } diff --git a/packages/typegen/tests/e2e/fmodata-preserve-customizations.test.ts b/packages/typegen/tests/e2e/fmodata-preserve-customizations.test.ts index 19c8d74..2c3fb3f 100644 --- a/packages/typegen/tests/e2e/fmodata-preserve-customizations.test.ts +++ b/packages/typegen/tests/e2e/fmodata-preserve-customizations.test.ts @@ -270,4 +270,54 @@ describe("fmodata generateODataTypes preserves user customizations", () => { await fs.rm(tmpDir, { recursive: true, force: true }); } }); + + it("preserves custom validators and removes stale files when clearOldFiles is true", async () => { + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-fmodata-preserve-")); + + try { + const entitySetName = "MyTable"; + const entityTypeName = "NS.MyTable"; + const metadata = makeMetadata({ + entitySetName, + entityTypeName, + fields: [{ name: "FieldA", type: "Edm.String", fieldId: "F1" }], + }); + + const existingFilePath = path.join(tmpDir, "MyTable.ts"); + await fs.writeFile( + existingFilePath, + [ + `import { fmTableOccurrence, textField } from "@proofkit/fmodata";`, + `import { z } from "zod/v4";`, + "", + `export const MyTable = fmTableOccurrence("MyTable", {`, + ` "FieldA": textField().inputValidator(z.string()).entityId("F1"),`, + "});", + "", + ].join("\n"), + "utf8", + ); + + const staleFilePath = path.join(tmpDir, "OldTable.ts"); + await fs.writeFile(staleFilePath, `export const OldTable = "stale";\n`, "utf8"); + + await generateODataTypes(metadata, { + type: "fmodata", + path: tmpDir, + clearOldFiles: true, + tables: [{ tableName: "MyTable" }], + }); + + const regenerated = await fs.readFile(existingFilePath, "utf8"); + expect(regenerated).toContain(`FieldA: textField().entityId("F1").inputValidator(z.string())`); + + const staleExists = await fs + .access(staleFilePath) + .then(() => true) + .catch(() => false); + expect(staleExists).toBe(false); + } finally { + await fs.rm(tmpDir, { recursive: true, force: true }); + } + }); });