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
46 changes: 39 additions & 7 deletions graphql/codegen/src/__tests__/codegen/expand-targets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';

import { expandApiNamesToMultiTarget, expandSchemaDirToMultiTarget, removeStaleTargetDirs } from '../../core/generate';
import { expandApiNamesToMultiTarget, expandSchemaDirToMultiTarget, removeStaleTargetDirs, TARGETS_MANIFEST } from '../../core/generate';

describe('expandApiNamesToMultiTarget', () => {
it('returns null for no apiNames', () => {
Expand Down Expand Up @@ -151,7 +151,13 @@ describe('removeStaleTargetDirs', () => {
fs.rmSync(tempDir, { recursive: true, force: true });
});

it('removes directories not in current target list', () => {
/** Write a .targets manifest listing the given names. */
function writeManifest(names: string[]) {
fs.writeFileSync(path.join(tempDir, TARGETS_MANIFEST), JSON.stringify(names));
}

it('removes previous targets that are no longer current', () => {
writeManifest(['admin', 'auth', 'public', 'objects']);
fs.mkdirSync(path.join(tempDir, 'admin'));
fs.mkdirSync(path.join(tempDir, 'auth'));
fs.mkdirSync(path.join(tempDir, 'public'));
Expand All @@ -166,35 +172,61 @@ describe('removeStaleTargetDirs', () => {
expect(fs.existsSync(path.join(tempDir, 'objects'))).toBe(false);
});

it('preserves files (only removes directories)', () => {
it('preserves directories not listed in manifest', () => {
writeManifest(['admin', 'auth']);
fs.mkdirSync(path.join(tempDir, 'admin'));
fs.writeFileSync(path.join(tempDir, 'index.ts'), 'export {}');
fs.mkdirSync(path.join(tempDir, 'auth'));
fs.mkdirSync(path.join(tempDir, 'config'));
fs.mkdirSync(path.join(tempDir, 'utils'));

const removed = removeStaleTargetDirs(tempDir, ['admin']);

expect(removed).toEqual(['auth']);
expect(fs.existsSync(path.join(tempDir, 'admin'))).toBe(true);
expect(fs.existsSync(path.join(tempDir, 'auth'))).toBe(false);
expect(fs.existsSync(path.join(tempDir, 'config'))).toBe(true);
expect(fs.existsSync(path.join(tempDir, 'utils'))).toBe(true);
});

it('returns empty array when no manifest exists', () => {
fs.mkdirSync(path.join(tempDir, 'admin'));
fs.mkdirSync(path.join(tempDir, 'config'));

const removed = removeStaleTargetDirs(tempDir, ['admin']);

expect(removed).toEqual([]);
expect(fs.existsSync(path.join(tempDir, 'index.ts'))).toBe(true);
expect(fs.existsSync(path.join(tempDir, 'config'))).toBe(true);
});

it('returns empty array when output root does not exist', () => {
const removed = removeStaleTargetDirs('/nonexistent/path', ['admin']);
expect(removed).toEqual([]);
});

it('returns empty array when no stale directories exist', () => {
it('returns empty array when no stale targets exist', () => {
writeManifest(['admin', 'auth']);
fs.mkdirSync(path.join(tempDir, 'admin'));
fs.mkdirSync(path.join(tempDir, 'auth'));

const removed = removeStaleTargetDirs(tempDir, ['admin', 'auth']);
expect(removed).toEqual([]);
});

it('removes all directories when target list is empty', () => {
it('removes all previous targets when current list is empty', () => {
writeManifest(['old-target']);
fs.mkdirSync(path.join(tempDir, 'old-target'));

const removed = removeStaleTargetDirs(tempDir, []);

expect(removed).toEqual(['old-target']);
expect(fs.existsSync(path.join(tempDir, 'old-target'))).toBe(false);
});

it('handles corrupt manifest gracefully', () => {
fs.writeFileSync(path.join(tempDir, TARGETS_MANIFEST), 'not json');
fs.mkdirSync(path.join(tempDir, 'admin'));

const removed = removeStaleTargetDirs(tempDir, []);
expect(removed).toEqual([]);
});
});
39 changes: 31 additions & 8 deletions graphql/codegen/src/core/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -626,9 +626,14 @@ function applySharedPgpmDb(
};
}

/** Manifest file listing generated target names, written to the output root. */
export const TARGETS_MANIFEST = '.targets';

/**
* Remove subdirectories in `outputRoot` that are not in `currentTargetNames`.
* Useful for cleaning up stale target output before a fresh multi-target generate.
* Remove stale generated target directories from `outputRoot`.
* Reads the `.targets` manifest (written by `generateMulti`) to know which
* directories were previously generated. Only those are eligible for removal;
* hand-written directories (e.g. `config/`, `utils/`) are never touched.
* Returns the list of directory names that were removed.
*/
export function removeStaleTargetDirs(
Expand All @@ -639,14 +644,26 @@ export function removeStaleTargetDirs(
const removed: string[] = [];
if (!fs.existsSync(outputRoot)) return removed;

const manifestPath = path.join(outputRoot, TARGETS_MANIFEST);
if (!fs.existsSync(manifestPath)) return removed;

let previousTargets: string[];
try {
previousTargets = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
} catch {
return removed;
}

const currentTargets = new Set(currentTargetNames);
const entries = fs.readdirSync(outputRoot, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && !currentTargets.has(entry.name)) {
fs.rmSync(path.join(outputRoot, entry.name), { recursive: true, force: true });
removed.push(entry.name);
const staleTargets = previousTargets.filter((t) => !currentTargets.has(t));

for (const target of staleTargets) {
const dirPath = path.join(outputRoot, target);
if (fs.existsSync(dirPath)) {
fs.rmSync(dirPath, { recursive: true, force: true });
removed.push(target);
if (verbose) {
console.log(`Removed stale target directory: ${entry.name}`);
console.log(`Removed stale target directory: ${target}`);
}
}
}
Expand Down Expand Up @@ -833,6 +850,12 @@ export async function generateMulti(
[],
{ pruneStaleFiles: false },
);

// Write manifest so removeStaleTargetDirs knows which dirs are generated
fs.writeFileSync(
path.join(outputRoot, TARGETS_MANIFEST),
JSON.stringify(successfulNames.sort()) + '\n',
);
}
}

Expand Down
2 changes: 1 addition & 1 deletion graphql/codegen/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export { defineConfig } from './types/config';

// Main generate function (orchestrates the entire pipeline)
export type { GenerateOptions, GenerateResult, GenerateMultiOptions, GenerateMultiResult } from './core/generate';
export { generate, generateMulti, expandApiNamesToMultiTarget, expandSchemaDirToMultiTarget, removeStaleTargetDirs } from './core/generate';
export { generate, generateMulti, expandApiNamesToMultiTarget, expandSchemaDirToMultiTarget, removeStaleTargetDirs, TARGETS_MANIFEST } from './core/generate';

// Config utilities
export { findConfigFile, loadConfigFile } from './core/config';
Expand Down
Loading