From a1cf8cd55c70859949a4a6bac9607c2847a0f75f Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Sun, 31 May 2026 04:06:52 +0000 Subject: [PATCH 1/2] feat(codegen): add cleanStaleTargets option to generateMulti MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add removeStaleTargetDirs() utility and cleanStaleTargets option to generateMulti(). When enabled, scans the output root and removes subdirectories that don't match any current target name — eliminating the need for pregenerate scripts with hardcoded directory lists. Exported as a standalone function for direct use by generate scripts. --- .../__tests__/codegen/expand-targets.test.ts | 61 ++++++++++++++++++- graphql/codegen/src/core/generate.ts | 38 +++++++++++- graphql/codegen/src/index.ts | 2 +- 3 files changed, 98 insertions(+), 3 deletions(-) diff --git a/graphql/codegen/src/__tests__/codegen/expand-targets.test.ts b/graphql/codegen/src/__tests__/codegen/expand-targets.test.ts index 1dc7ee33c1..6d405cf4b7 100644 --- a/graphql/codegen/src/__tests__/codegen/expand-targets.test.ts +++ b/graphql/codegen/src/__tests__/codegen/expand-targets.test.ts @@ -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 } from '../../core/generate'; +import { expandApiNamesToMultiTarget, expandSchemaDirToMultiTarget, removeStaleTargetDirs } from '../../core/generate'; describe('expandApiNamesToMultiTarget', () => { it('returns null for no apiNames', () => { @@ -139,3 +139,62 @@ describe('expandSchemaDirToMultiTarget', () => { expect(Object.keys(result!)).toEqual(['alpha', 'mid', 'zebra']); }); }); + +describe('removeStaleTargetDirs', () => { + let tempDir: string; + + beforeEach(() => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'stale-targets-')); + }); + + afterEach(() => { + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + it('removes directories not in current target list', () => { + fs.mkdirSync(path.join(tempDir, 'admin')); + fs.mkdirSync(path.join(tempDir, 'auth')); + fs.mkdirSync(path.join(tempDir, 'public')); + fs.mkdirSync(path.join(tempDir, 'objects')); + + const removed = removeStaleTargetDirs(tempDir, ['admin', 'auth']); + + expect(removed.sort()).toEqual(['objects', 'public']); + expect(fs.existsSync(path.join(tempDir, 'admin'))).toBe(true); + expect(fs.existsSync(path.join(tempDir, 'auth'))).toBe(true); + expect(fs.existsSync(path.join(tempDir, 'public'))).toBe(false); + expect(fs.existsSync(path.join(tempDir, 'objects'))).toBe(false); + }); + + it('preserves files (only removes directories)', () => { + fs.mkdirSync(path.join(tempDir, 'admin')); + fs.writeFileSync(path.join(tempDir, 'index.ts'), 'export {}'); + + const removed = removeStaleTargetDirs(tempDir, ['admin']); + + expect(removed).toEqual([]); + expect(fs.existsSync(path.join(tempDir, 'index.ts'))).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', () => { + 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', () => { + 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); + }); +}); diff --git a/graphql/codegen/src/core/generate.ts b/graphql/codegen/src/core/generate.ts index ee5a3130fc..026d117f6c 100644 --- a/graphql/codegen/src/core/generate.ts +++ b/graphql/codegen/src/core/generate.ts @@ -510,6 +510,8 @@ export interface GenerateMultiOptions { dryRun?: boolean; schema?: SchemaConfig; unifiedCli?: CliConfig | boolean; + /** Remove subdirectories in the output root that don't match any current target name. */ + cleanStaleTargets?: boolean; } export interface GenerateMultiResult { @@ -624,10 +626,37 @@ function applySharedPgpmDb( }; } +/** + * Remove subdirectories in `outputRoot` that are not in `currentTargetNames`. + * Useful for cleaning up stale target output before a fresh multi-target generate. + * Returns the list of directory names that were removed. + */ +export function removeStaleTargetDirs( + outputRoot: string, + currentTargetNames: string[], + verbose?: boolean, +): string[] { + const removed: string[] = []; + if (!fs.existsSync(outputRoot)) 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); + if (verbose) { + console.log(`Removed stale target directory: ${entry.name}`); + } + } + } + return removed; +} + export async function generateMulti( options: GenerateMultiOptions, ): Promise { - const { configs, cliOverrides, verbose, dryRun, schema, unifiedCli } = options; + const { configs, cliOverrides, verbose, dryRun, schema, unifiedCli, cleanStaleTargets } = options; const names = Object.keys(configs); const results: Array<{ name: string; result: GenerateResult }> = []; let hasError = false; @@ -638,6 +667,13 @@ export async function generateMulti( const cliTargets: MultiTargetCliTarget[] = []; + // Remove stale target directories before generating + if (cleanStaleTargets && names.length > 0 && !dryRun) { + const firstOutput = getConfigOptions(configs[names[0]]).output; + const outputRoot = path.dirname(firstOutput); + removeStaleTargetDirs(outputRoot, names, verbose); + } + const sharedSources = await prepareSharedPgpmSources(configs, cliOverrides); try { diff --git a/graphql/codegen/src/index.ts b/graphql/codegen/src/index.ts index 56ea02d77a..a8a417da4e 100644 --- a/graphql/codegen/src/index.ts +++ b/graphql/codegen/src/index.ts @@ -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 } from './core/generate'; +export { generate, generateMulti, expandApiNamesToMultiTarget, expandSchemaDirToMultiTarget, removeStaleTargetDirs } from './core/generate'; // Config utilities export { findConfigFile, loadConfigFile } from './core/config'; From 0c94622d1b446cf3bf9e38715504a305ab7787b4 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Sun, 31 May 2026 04:10:49 +0000 Subject: [PATCH 2/2] feat(sdk): pass cleanStaleTargets and remove pregenerate scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All three SDK generate scripts (constructive-sdk, constructive-cli, constructive-react) now pass cleanStaleTargets: true to generateMulti. Remove the pregenerate npm scripts with hardcoded target directories — stale directory cleanup is now handled automatically by the codegen. --- sdk/constructive-cli/package.json | 3 +-- sdk/constructive-cli/scripts/generate-sdk.ts | 1 + sdk/constructive-react/package.json | 3 +-- sdk/constructive-react/scripts/generate-react.ts | 1 + sdk/constructive-sdk/package.json | 3 +-- sdk/constructive-sdk/scripts/generate-sdk.ts | 1 + 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sdk/constructive-cli/package.json b/sdk/constructive-cli/package.json index f817bab369..2a462b0886 100644 --- a/sdk/constructive-cli/package.json +++ b/sdk/constructive-cli/package.json @@ -27,8 +27,7 @@ "prepack": "npm run build", "build": "makage build", "build:dev": "makage build --dev", - "pregenerate": "rimraf src/admin src/auth src/objects src/public src/index.ts", - "generate": "pnpm run pregenerate && tsx scripts/generate-sdk.ts", + "generate": "rimraf src/index.ts && tsx scripts/generate-sdk.ts", "lint": "eslint . --fix", "test": "jest --passWithNoTests", "test:watch": "jest --watch" diff --git a/sdk/constructive-cli/scripts/generate-sdk.ts b/sdk/constructive-cli/scripts/generate-sdk.ts index c2d892067e..981cff1fce 100644 --- a/sdk/constructive-cli/scripts/generate-sdk.ts +++ b/sdk/constructive-cli/scripts/generate-sdk.ts @@ -47,6 +47,7 @@ async function main() { const { results, hasError } = await generateMulti({ configs: expanded, + cleanStaleTargets: true, }); let realError = false; diff --git a/sdk/constructive-react/package.json b/sdk/constructive-react/package.json index 1884b69928..8242b1574b 100644 --- a/sdk/constructive-react/package.json +++ b/sdk/constructive-react/package.json @@ -24,8 +24,7 @@ "prepack": "npm run build", "build": "makage build", "build:dev": "makage build --dev", - "pregenerate": "rimraf src/admin src/auth src/objects src/public src/index.ts", - "generate": "pnpm run pregenerate && tsx scripts/generate-react.ts", + "generate": "rimraf src/index.ts && tsx scripts/generate-react.ts", "lint": "eslint . --fix", "test": "jest --passWithNoTests", "test:watch": "jest --watch" diff --git a/sdk/constructive-react/scripts/generate-react.ts b/sdk/constructive-react/scripts/generate-react.ts index 2a7ceb1693..511b7bae8a 100644 --- a/sdk/constructive-react/scripts/generate-react.ts +++ b/sdk/constructive-react/scripts/generate-react.ts @@ -43,6 +43,7 @@ async function main() { const { results, hasError } = await generateMulti({ configs: expanded, + cleanStaleTargets: true, }); let realError = false; diff --git a/sdk/constructive-sdk/package.json b/sdk/constructive-sdk/package.json index 641c29d356..d4b6f26526 100644 --- a/sdk/constructive-sdk/package.json +++ b/sdk/constructive-sdk/package.json @@ -24,8 +24,7 @@ "prepack": "npm run build", "build": "makage build", "build:dev": "makage build --dev", - "pregenerate": "rimraf src/admin src/auth src/objects src/public src/index.ts", - "generate": "pnpm run pregenerate && tsx scripts/generate-sdk.ts", + "generate": "rimraf src/index.ts && tsx scripts/generate-sdk.ts", "pregenerate:migrate-client": "rimraf ../migrate-client/src/migrate", "generate:migrate-client": "pnpm run pregenerate:migrate-client && tsx scripts/generate-migrate-client.ts", "lint": "eslint . --fix", diff --git a/sdk/constructive-sdk/scripts/generate-sdk.ts b/sdk/constructive-sdk/scripts/generate-sdk.ts index 249823e833..eb35d1ec13 100644 --- a/sdk/constructive-sdk/scripts/generate-sdk.ts +++ b/sdk/constructive-sdk/scripts/generate-sdk.ts @@ -43,6 +43,7 @@ async function main() { const { results, hasError } = await generateMulti({ configs: expanded, + cleanStaleTargets: true, }); let realError = false;