Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
34 changes: 28 additions & 6 deletions packages/typegen/src/fmodata/generateODataTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1055,24 +1055,20 @@ 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<string>();

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

// 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);
Expand Down Expand Up @@ -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)}";`);
Expand All @@ -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);
}
}
50 changes: 50 additions & 0 deletions packages/typegen/tests/e2e/fmodata-preserve-customizations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
});
});
Loading