From bc02680a345ff948eb35a90a1caf20bc1803a9bc Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 23:25:28 +0000 Subject: [PATCH 1/2] Add POC patch + test for overwrite.removeStaleFiles bug in @graphql-codegen/cli The server preset hard-codes overwrite.removeStaleFiles=false so watch mode doesn't delete resolver files. But @graphql-codegen/cli's normalizeOverwriteConfig() looks up the matching `generates` entry by an exact match on each generated file's own path, and requires that entry to have a `plugins` key. Preset-based outputs are keyed by baseOutputDir (not per-file) and have no `plugins` key, so the lookup always misses and Codegen silently falls back to the global default (removeStaleFiles: true). Add a pnpm patch for @graphql-codegen/cli@7.3.1 that matches `generates` entries by path-prefix and also recognizes preset-based outputs, plus a test that fails against the unpatched dependency and passes against the patched one. This is a local proof of concept to demonstrate and validate the fix - the real fix still needs to land upstream in graphql-code-generator/graphql-code-generator. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01QRhgmWtrzQAFoN47ebXrkY --- .../typescript-resolver-files/package.json | 3 + .../src/defineConfig.staleFilesCliBug.spec.ts | 102 ++++++++++++++++++ patches/@graphql-codegen__cli@7.3.1.patch | 93 ++++++++++++++++ pnpm-lock.yaml | 17 ++- pnpm-workspace.yaml | 2 + 5 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 packages/typescript-resolver-files/src/defineConfig.staleFilesCliBug.spec.ts create mode 100644 patches/@graphql-codegen__cli@7.3.1.patch diff --git a/packages/typescript-resolver-files/package.json b/packages/typescript-resolver-files/package.json index 87cd817f..942dd0ba 100644 --- a/packages/typescript-resolver-files/package.json +++ b/packages/typescript-resolver-files/package.json @@ -52,6 +52,9 @@ "ts-morph": "^22.0.0", "tslib": "^2.8.0" }, + "devDependencies": { + "@graphql-codegen/cli": "^7.3.1" + }, "files": [ "dist", "!**/*.tsbuildinfo" diff --git a/packages/typescript-resolver-files/src/defineConfig.staleFilesCliBug.spec.ts b/packages/typescript-resolver-files/src/defineConfig.staleFilesCliBug.spec.ts new file mode 100644 index 00000000..7ef144f4 --- /dev/null +++ b/packages/typescript-resolver-files/src/defineConfig.staleFilesCliBug.spec.ts @@ -0,0 +1,102 @@ +import { createRequire } from 'module'; +import * as path from 'path'; +import { pathToFileURL } from 'url'; +import { defineConfig } from './defineConfig.js'; + +/** + * `@graphql-codegen/cli`'s `overwrite` resolution (`normalizeOverwriteConfig` + * in `generate-and-save.js`) looks up the matching `codegen.ts` `generates` + * entry by doing an *exact* match on the per-file path Codegen is about to + * write, then requires that entry to have a `plugins` key. + * + * The server preset's `generates` entry is keyed by `baseOutputDir` and has + * no `plugins` key (the preset supplies plugins internally), while every + * file it writes lives *underneath* that key. So the exact-match lookup + * always misses and `overwrite.removeStaleFiles: false` from `defineConfig()` + * (defineConfig.ts) is silently ignored - Codegen falls back to the global + * default of `removeStaleFiles: true`, deleting resolver files in watch mode. + * + * This is only reachable through `@graphql-codegen/cli`'s internals (not + * exported publicly), so this repo carries a `pnpm patch` for + * `@graphql-codegen/cli` (see `patches/@graphql-codegen__cli@7.3.1.patch`) + * that fixes the lookup and exposes `normalizeOverwriteConfig` for this test. + */ +async function loadNormalizeOverwriteConfig() { + const require = createRequire(import.meta.url); + const cliPackageJsonPath = require.resolve('@graphql-codegen/cli/package.json'); + const generateAndSavePath = path.join( + path.dirname(cliPackageJsonPath), + 'esm', + 'generate-and-save.js' + ); + const mod = await import(pathToFileURL(generateAndSavePath).href); + return mod.normalizeOverwriteConfig as ( + config: { overwrite?: unknown; generates: Record }, + outputPath: string + ) => { removeStaleFiles: boolean; updateExistingFiles: boolean }; +} + +describe('server preset overwrite vs @graphql-codegen/cli stale file detection', () => { + it('honors defineConfig()’s overwrite for a file nested under baseOutputDir', async () => { + const normalizeOverwriteConfig = await loadNormalizeOverwriteConfig(); + const baseOutputDir = 'src/schema'; + const outputConfig = defineConfig({}, { baseOutputDir }); + + const config = { + overwrite: true, // global default codegen.ts would otherwise fall back to + generates: { + [baseOutputDir]: outputConfig, + }, + }; + + // A file the server preset actually writes, nested under baseOutputDir. + const generatedFilePath = path.posix.join( + baseOutputDir, + 'base', + 'resolvers', + 'Query', + 'user.ts' + ); + + expect(normalizeOverwriteConfig(config, generatedFilePath)).toEqual({ + removeStaleFiles: false, + updateExistingFiles: true, + }); + }); + + it('still honors an exact-match generates key (non-preset outputs)', async () => { + const normalizeOverwriteConfig = await loadNormalizeOverwriteConfig(); + const outputPath = 'src/generated.ts'; + const config = { + overwrite: true, + generates: { + [outputPath]: { + plugins: ['typescript'], + overwrite: { removeStaleFiles: false, updateExistingFiles: false }, + }, + }, + }; + + expect(normalizeOverwriteConfig(config, outputPath)).toEqual({ + removeStaleFiles: false, + updateExistingFiles: false, + }); + }); + + it('falls back to the global overwrite for a path outside any generates entry', async () => { + const normalizeOverwriteConfig = await loadNormalizeOverwriteConfig(); + const config = { + overwrite: { removeStaleFiles: false, updateExistingFiles: false }, + generates: { + 'src/schema': defineConfig({}, { baseOutputDir: 'src/schema' }), + }, + }; + + expect( + normalizeOverwriteConfig(config, 'some/unrelated/file.ts') + ).toEqual({ + removeStaleFiles: false, + updateExistingFiles: false, + }); + }); +}); diff --git a/patches/@graphql-codegen__cli@7.3.1.patch b/patches/@graphql-codegen__cli@7.3.1.patch new file mode 100644 index 00000000..2e8a7ab8 --- /dev/null +++ b/patches/@graphql-codegen__cli@7.3.1.patch @@ -0,0 +1,93 @@ +diff --git a/cjs/generate-and-save.js b/cjs/generate-and-save.js +index 0fe6eb8d696e70248b7e4bf14d70c180a128affc..cba87434315f05b10621931f9e960cf117a17546 100644 +--- a/cjs/generate-and-save.js ++++ b/cjs/generate-and-save.js +@@ -135,7 +135,7 @@ async function generate(input, saveToFile = true) { + function normalizeOverwriteConfig(config, outputPath) { + const overwrite = (function getOverwriteOption() { + const { overwrite: result = true } = config; +- const outputConfig = config.generates[outputPath]; ++ const outputConfig = findOutputConfig(config.generates, outputPath); + if (!outputConfig) { + (0, debugging_js_1.debugLog)(`Couldn't find a config of ${outputPath}`); + return result; +@@ -160,9 +160,30 @@ function normalizeOverwriteConfig(config, outputPath) { + const { removeStaleFiles = true, updateExistingFiles = true } = overwrite; + return { removeStaleFiles, updateExistingFiles }; + } ++function findOutputConfig(generates, outputPath) { ++ if (generates[outputPath]) { ++ return generates[outputPath]; ++ } ++ // Presets (e.g. the server preset from @eddeee888/gcg-typescript-resolver-files) ++ // write many files nested under a single `generates` entry keyed by the ++ // preset's base output directory, so an exact match against the per-file ++ // `outputPath` never succeeds. Fall back to the longest `generates` key ++ // that is a path-prefix of `outputPath`. ++ let bestMatch; ++ let bestMatchKeyLength = -1; ++ for (const key of Object.keys(generates)) { ++ const isPrefix = outputPath === key || outputPath.startsWith(`${key}/`); ++ if (isPrefix && key.length > bestMatchKeyLength) { ++ bestMatch = generates[key]; ++ bestMatchKeyLength = key.length; ++ } ++ } ++ return bestMatch; ++} + function isConfiguredOutput(output) { +- return typeof output.plugins !== 'undefined'; ++ return typeof output.plugins !== 'undefined' || typeof output.preset !== 'undefined'; + } ++exports.normalizeOverwriteConfig = normalizeOverwriteConfig; + async function hashFile(filePath) { + try { + return hash(await (0, file_system_js_1.readFile)(filePath)); +diff --git a/esm/generate-and-save.js b/esm/generate-and-save.js +index be32dbb79ebe8adaae8f98454d63c70eef4b3ca6..bc8bfbeb4c408ce03e9581509d47069913026965 100644 +--- a/esm/generate-and-save.js ++++ b/esm/generate-and-save.js +@@ -131,7 +131,7 @@ export async function generate(input, saveToFile = true) { + function normalizeOverwriteConfig(config, outputPath) { + const overwrite = (function getOverwriteOption() { + const { overwrite: result = true } = config; +- const outputConfig = config.generates[outputPath]; ++ const outputConfig = findOutputConfig(config.generates, outputPath); + if (!outputConfig) { + debugLog(`Couldn't find a config of ${outputPath}`); + return result; +@@ -156,8 +156,28 @@ function normalizeOverwriteConfig(config, outputPath) { + const { removeStaleFiles = true, updateExistingFiles = true } = overwrite; + return { removeStaleFiles, updateExistingFiles }; + } ++function findOutputConfig(generates, outputPath) { ++ if (generates[outputPath]) { ++ return generates[outputPath]; ++ } ++ // Presets (e.g. the server preset from @eddeee888/gcg-typescript-resolver-files) ++ // write many files nested under a single `generates` entry keyed by the ++ // preset's base output directory, so an exact match against the per-file ++ // `outputPath` never succeeds. Fall back to the longest `generates` key ++ // that is a path-prefix of `outputPath`. ++ let bestMatch; ++ let bestMatchKeyLength = -1; ++ for (const key of Object.keys(generates)) { ++ const isPrefix = outputPath === key || outputPath.startsWith(`${key}/`); ++ if (isPrefix && key.length > bestMatchKeyLength) { ++ bestMatch = generates[key]; ++ bestMatchKeyLength = key.length; ++ } ++ } ++ return bestMatch; ++} + function isConfiguredOutput(output) { +- return typeof output.plugins !== 'undefined'; ++ return typeof output.plugins !== 'undefined' || typeof output.preset !== 'undefined'; + } + async function hashFile(filePath) { + try { +@@ -172,3 +192,4 @@ async function hashFile(filePath) { + throw err; + } + } ++export { normalizeOverwriteConfig }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a16972e8..59307f31 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -203,6 +203,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +patchedDependencies: + '@graphql-codegen/cli@7.3.1': b344808ec016720bb03048ccf854992022c6a9d435ef8223a512fc48843ea8e7 + importers: .: @@ -219,7 +222,7 @@ importers: version: 1.0.0(jiti@2.7.0)(typescript@6.0.3) '@eddeee888/nx-graphql-code-generator': specifier: 2.1.0 - version: 2.1.0(@graphql-codegen/cli@7.3.1(@types/node@22.20.1)(graphql@17.0.2)(typescript@6.0.3))(nx@23.1.1(@swc-node/register@1.12.1(@swc/core@1.15.8(@swc/helpers@0.5.23))(@swc/types@0.1.28)(typescript@6.0.3))(@swc/core@1.15.8(@swc/helpers@0.5.23))) + version: 2.1.0(@graphql-codegen/cli@7.3.1(patch_hash=b344808ec016720bb03048ccf854992022c6a9d435ef8223a512fc48843ea8e7)(@types/node@22.20.1)(graphql@17.0.2)(typescript@6.0.3))(nx@23.1.1(@swc-node/register@1.12.1(@swc/core@1.15.8(@swc/helpers@0.5.23))(@swc/types@0.1.28)(typescript@6.0.3))(@swc/core@1.15.8(@swc/helpers@0.5.23))) '@eslint/eslintrc': specifier: 3.3.1 version: 3.3.1 @@ -228,7 +231,7 @@ importers: version: 7.1.0(graphql@17.0.2) '@graphql-codegen/cli': specifier: 7.3.1 - version: 7.3.1(@types/node@22.20.1)(graphql@17.0.2)(typescript@6.0.3) + version: 7.3.1(patch_hash=b344808ec016720bb03048ccf854992022c6a9d435ef8223a512fc48843ea8e7)(@types/node@22.20.1)(graphql@17.0.2)(typescript@6.0.3) '@graphql-codegen/near-operation-file-preset': specifier: 5.2.1 version: 5.2.1(graphql@17.0.2) @@ -436,6 +439,10 @@ importers: tslib: specifier: ^2.8.0 version: 2.8.1 + devDependencies: + '@graphql-codegen/cli': + specifier: ^7.3.1 + version: 7.3.1(patch_hash=b344808ec016720bb03048ccf854992022c6a9d435ef8223a512fc48843ea8e7)(@types/node@22.20.1)(graphql@17.0.2)(typescript@6.0.3) packages/typescript-resolver-files-e2e: devDependencies: @@ -7364,9 +7371,9 @@ snapshots: - supports-color - typescript - '@eddeee888/nx-graphql-code-generator@2.1.0(@graphql-codegen/cli@7.3.1(@types/node@22.20.1)(graphql@17.0.2)(typescript@6.0.3))(nx@23.1.1(@swc-node/register@1.12.1(@swc/core@1.15.8(@swc/helpers@0.5.23))(@swc/types@0.1.28)(typescript@6.0.3))(@swc/core@1.15.8(@swc/helpers@0.5.23)))': + '@eddeee888/nx-graphql-code-generator@2.1.0(@graphql-codegen/cli@7.3.1(patch_hash=b344808ec016720bb03048ccf854992022c6a9d435ef8223a512fc48843ea8e7)(@types/node@22.20.1)(graphql@17.0.2)(typescript@6.0.3))(nx@23.1.1(@swc-node/register@1.12.1(@swc/core@1.15.8(@swc/helpers@0.5.23))(@swc/types@0.1.28)(typescript@6.0.3))(@swc/core@1.15.8(@swc/helpers@0.5.23)))': dependencies: - '@graphql-codegen/cli': 7.3.1(@types/node@22.20.1)(graphql@17.0.2)(typescript@6.0.3) + '@graphql-codegen/cli': 7.3.1(patch_hash=b344808ec016720bb03048ccf854992022c6a9d435ef8223a512fc48843ea8e7)(@types/node@22.20.1)(graphql@17.0.2)(typescript@6.0.3) '@nx/devkit': 20.8.4(nx@23.1.1(@swc-node/register@1.12.1(@swc/core@1.15.8(@swc/helpers@0.5.23))(@swc/types@0.1.28)(typescript@6.0.3))(@swc/core@1.15.8(@swc/helpers@0.5.23))) semver: 7.8.5 tslib: 2.8.1 @@ -7583,7 +7590,7 @@ snapshots: graphql: 17.0.2 tslib: 2.8.1 - '@graphql-codegen/cli@7.3.1(@types/node@22.20.1)(graphql@17.0.2)(typescript@6.0.3)': + '@graphql-codegen/cli@7.3.1(patch_hash=b344808ec016720bb03048ccf854992022c6a9d435ef8223a512fc48843ea8e7)(@types/node@22.20.1)(graphql@17.0.2)(typescript@6.0.3)': dependencies: '@babel/generator': 7.29.8 '@babel/template': 7.29.7 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e0d1aa41..47f3cab6 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -9,3 +9,5 @@ allowBuilds: minimumReleaseAgeExclude: - '@graphql-codegen/typescript-react-apollo@4.4.3-alpha-20260808101442-e439530add2acc6e119f54b148ccb537e4d0a7ec' - '@graphql-codegen/plugin-helpers@7.2.0' +patchedDependencies: + '@graphql-codegen/cli@7.3.1': patches/@graphql-codegen__cli@7.3.1.patch From f968b936dc832d1de674166b7efd699848088edd Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 23:42:34 +0000 Subject: [PATCH 2/2] Rework patch: carry overwrite on each generated file instead of path matching Replaces the path-prefix lookup with the approach codegen.js already uses for `hooks`: `process` in codegen.js has both the `generates` key and the per-file path in scope, so it now tags each result with that entry's `overwrite`. `normalizeOverwriteConfig` then reads `fileOutput.overwrite` directly and needs no lookup, so `findOutputConfig` and the `plugins`-only `isConfiguredOutput` gate are both gone - the function shrinks rather than grows. Prefix matching was also not just inelegant but wrong: a preset can emit files whose paths are not under its baseOutputDir at all (the server preset does), and those would still have missed. `removeStaleFiles` now retains the previous run's {filename, overwrite} pairs so a file that disappears is judged by the entry that produced it. Tests exercise the CLI contract with a stub preset - a directory-keyed entry with no `plugins` key emitting a path outside its base dir - keeping them independent of the server preset's internals and of graphql module identity. All four fail against the unpatched dependency and pass against the patched one. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QRhgmWtrzQAFoN47ebXrkY --- .../src/defineConfig.staleFilesCliBug.spec.ts | 177 ++++++++----- patches/@graphql-codegen__cli@7.3.1.patch | 245 +++++++++++++----- pnpm-lock.yaml | 14 +- 3 files changed, 294 insertions(+), 142 deletions(-) diff --git a/packages/typescript-resolver-files/src/defineConfig.staleFilesCliBug.spec.ts b/packages/typescript-resolver-files/src/defineConfig.staleFilesCliBug.spec.ts index 7ef144f4..b1f00815 100644 --- a/packages/typescript-resolver-files/src/defineConfig.staleFilesCliBug.spec.ts +++ b/packages/typescript-resolver-files/src/defineConfig.staleFilesCliBug.spec.ts @@ -1,29 +1,95 @@ import { createRequire } from 'module'; import * as path from 'path'; import { pathToFileURL } from 'url'; -import { defineConfig } from './defineConfig.js'; +import { executeCodegen } from '@graphql-codegen/cli'; /** - * `@graphql-codegen/cli`'s `overwrite` resolution (`normalizeOverwriteConfig` - * in `generate-and-save.js`) looks up the matching `codegen.ts` `generates` - * entry by doing an *exact* match on the per-file path Codegen is about to - * write, then requires that entry to have a `plugins` key. + * `@graphql-codegen/cli` used to resolve a generated file's `overwrite` option + * (`normalizeOverwriteConfig` in `generate-and-save.js`) by looking the file's + * own path up in `config.generates`, then requiring that entry to have a + * `plugins` key. * - * The server preset's `generates` entry is keyed by `baseOutputDir` and has - * no `plugins` key (the preset supplies plugins internally), while every - * file it writes lives *underneath* that key. So the exact-match lookup - * always misses and `overwrite.removeStaleFiles: false` from `defineConfig()` - * (defineConfig.ts) is silently ignored - Codegen falls back to the global - * default of `removeStaleFiles: true`, deleting resolver files in watch mode. + * Neither holds for a preset-based output such as the server preset: its + * `generates` entry is keyed by `baseOutputDir` (a directory it writes many + * files into, and some generated paths are not even under it), and it has no + * `plugins` key because the preset supplies plugins itself. So the lookup + * always missed, and `overwrite.removeStaleFiles: false` from `defineConfig()` + * was silently dropped in favour of Codegen's global default of + * `removeStaleFiles: true` - deleting resolver files in watch mode. * - * This is only reachable through `@graphql-codegen/cli`'s internals (not - * exported publicly), so this repo carries a `pnpm patch` for - * `@graphql-codegen/cli` (see `patches/@graphql-codegen__cli@7.3.1.patch`) - * that fixes the lookup and exposes `normalizeOverwriteConfig` for this test. + * The fix carries the `generates` entry's `overwrite` on each generated file + * (the same way `hooks` already was), so no path matching is needed at all. + * This repo carries it as `patches/@graphql-codegen__cli@7.3.1.patch` until it + * lands upstream. + * + * These tests exercise the CLI contract with a stub preset rather than the + * server preset itself, so they stay independent of what the server preset + * happens to emit. `defineConfig.spec.ts` covers the `overwrite` value the + * server preset declares. + */ + +const serverPresetOverwrite = { + removeStaleFiles: false, + updateExistingFiles: true, +}; + +const baseOutputDir = 'src/schema'; + +// Mirrors how the server preset emits files: many outputs from one `generates` +// entry keyed by a directory, including a path that is not under that directory. +const generatedFilenames = [ + `${baseOutputDir}/types.generated.ts`, + `${baseOutputDir}/resolvers/Query/user.ts`, + 'resolvers/User.ts', +]; + +const stubPreset = { + buildGeneratesSection: (options: Record) => + generatedFilenames.map((filename) => ({ + ...options, + filename, + plugins: [], + pluginMap: {}, + })), +}; + +describe('overwrite propagation through @graphql-codegen/cli', () => { + it('tags every file a preset generates with the generates entry’s overwrite', async () => { + const { result, error } = await executeCodegen({ + schema: 'type Query { user: User } type User { id: ID! name: String }', + generates: { + // No `plugins` key, keyed by a directory - exactly the server preset's shape. + [baseOutputDir]: { + preset: stubPreset, + overwrite: serverPresetOverwrite, + }, + }, + } as never); + + expect(error).toBeNull(); + expect(result.map((file) => file.filename)).toEqual(generatedFilenames); + + for (const file of result) { + expect({ + filename: file.filename, + overwrite: (file as { overwrite?: unknown }).overwrite, + }).toEqual({ + filename: file.filename, + overwrite: serverPresetOverwrite, + }); + } + }); +}); + +/** + * `normalizeOverwriteConfig` is internal to `generate-and-save.js`; the patch + * adds a named export so the resolution rules can be asserted directly. */ async function loadNormalizeOverwriteConfig() { const require = createRequire(import.meta.url); - const cliPackageJsonPath = require.resolve('@graphql-codegen/cli/package.json'); + const cliPackageJsonPath = require.resolve( + '@graphql-codegen/cli/package.json' + ); const generateAndSavePath = path.join( path.dirname(cliPackageJsonPath), 'esm', @@ -31,72 +97,47 @@ async function loadNormalizeOverwriteConfig() { ); const mod = await import(pathToFileURL(generateAndSavePath).href); return mod.normalizeOverwriteConfig as ( - config: { overwrite?: unknown; generates: Record }, - outputPath: string + config: { overwrite?: unknown }, + fileOutput: { filename: string; overwrite?: unknown } ) => { removeStaleFiles: boolean; updateExistingFiles: boolean }; } -describe('server preset overwrite vs @graphql-codegen/cli stale file detection', () => { - it('honors defineConfig()’s overwrite for a file nested under baseOutputDir', async () => { +describe('normalizeOverwriteConfig()', () => { + it('uses the overwrite carried on the generated file', async () => { const normalizeOverwriteConfig = await loadNormalizeOverwriteConfig(); - const baseOutputDir = 'src/schema'; - const outputConfig = defineConfig({}, { baseOutputDir }); - const config = { - overwrite: true, // global default codegen.ts would otherwise fall back to - generates: { - [baseOutputDir]: outputConfig, - }, - }; - - // A file the server preset actually writes, nested under baseOutputDir. - const generatedFilePath = path.posix.join( - baseOutputDir, - 'base', - 'resolvers', - 'Query', - 'user.ts' - ); - - expect(normalizeOverwriteConfig(config, generatedFilePath)).toEqual({ - removeStaleFiles: false, - updateExistingFiles: true, - }); + expect( + normalizeOverwriteConfig( + { overwrite: true }, + { + filename: `${baseOutputDir}/resolvers/Query/user.ts`, + overwrite: serverPresetOverwrite, + } + ) + ).toEqual(serverPresetOverwrite); }); - it('still honors an exact-match generates key (non-preset outputs)', async () => { + it('falls back to the global overwrite when the file carries none', async () => { const normalizeOverwriteConfig = await loadNormalizeOverwriteConfig(); - const outputPath = 'src/generated.ts'; - const config = { - overwrite: true, - generates: { - [outputPath]: { - plugins: ['typescript'], - overwrite: { removeStaleFiles: false, updateExistingFiles: false }, - }, - }, - }; - expect(normalizeOverwriteConfig(config, outputPath)).toEqual({ - removeStaleFiles: false, - updateExistingFiles: false, - }); + expect( + normalizeOverwriteConfig( + { overwrite: { removeStaleFiles: false } }, + { filename: 'src/generated.ts' } + ) + ).toEqual({ removeStaleFiles: false, updateExistingFiles: true }); }); - it('falls back to the global overwrite for a path outside any generates entry', async () => { + it('expands the boolean shorthand', async () => { const normalizeOverwriteConfig = await loadNormalizeOverwriteConfig(); - const config = { - overwrite: { removeStaleFiles: false, updateExistingFiles: false }, - generates: { - 'src/schema': defineConfig({}, { baseOutputDir: 'src/schema' }), - }, - }; expect( - normalizeOverwriteConfig(config, 'some/unrelated/file.ts') - ).toEqual({ - removeStaleFiles: false, - updateExistingFiles: false, + normalizeOverwriteConfig({}, { filename: 'a.ts', overwrite: false }) + ).toEqual({ removeStaleFiles: false, updateExistingFiles: false }); + + expect(normalizeOverwriteConfig({}, { filename: 'a.ts' })).toEqual({ + removeStaleFiles: true, + updateExistingFiles: true, }); }); }); diff --git a/patches/@graphql-codegen__cli@7.3.1.patch b/patches/@graphql-codegen__cli@7.3.1.patch index 2e8a7ab8..8666ac47 100644 --- a/patches/@graphql-codegen__cli@7.3.1.patch +++ b/patches/@graphql-codegen__cli@7.3.1.patch @@ -1,92 +1,203 @@ +diff --git a/cjs/codegen.js b/cjs/codegen.js +index 27e313bd7b700a4827567fd72b6328d0ca9a87c8..1d735b17f596cf5c2716cde521123ab08173567c 100644 +--- a/cjs/codegen.js ++++ b/cjs/codegen.js +@@ -378,6 +378,11 @@ async function executeCodegen(input) { + filename: outputArgs.filename, + content: output, + hooks: outputConfig.hooks || {}, ++ // Carry the `generates` entry's `overwrite` on the file ++ // itself. A preset writes many files under one entry keyed ++ // by its base output dir, so the entry cannot be recovered ++ // from the generated file's path later on. ++ overwrite: outputConfig.overwrite, + }); + }; + await context.profiler.run(() => Promise.all(outputs.map(process)), `Codegen: ${filename}`); diff --git a/cjs/generate-and-save.js b/cjs/generate-and-save.js -index 0fe6eb8d696e70248b7e4bf14d70c180a128affc..cba87434315f05b10621931f9e960cf117a17546 100644 +index 0fe6eb8d696e70248b7e4bf14d70c180a128affc..3ec481027a1ba66d0dc3054498ee09d833b6eea3 100644 --- a/cjs/generate-and-save.js +++ b/cjs/generate-and-save.js -@@ -135,7 +135,7 @@ async function generate(input, saveToFile = true) { - function normalizeOverwriteConfig(config, outputPath) { - const overwrite = (function getOverwriteOption() { - const { overwrite: result = true } = config; +@@ -17,13 +17,14 @@ async function generate(input, saveToFile = true) { + const context = (0, config_js_1.ensureContext)(input); + const config = context.getConfig(); + await context.profiler.run(() => (0, hooks_js_1.lifecycleHooks)(config.hooks).afterStart(), 'Lifecycle: afterStart'); +- let previouslyGeneratedFilenames = []; ++ let previouslyGeneratedFiles = []; + function removeStaleFiles(config, generationResult) { + const filenames = generationResult.map(o => o.filename); + // find stale files from previous build which are not present in current build +- const staleFilenames = previouslyGeneratedFilenames.filter(f => !filenames.includes(f)); +- for (const filename of staleFilenames) { +- if (normalizeOverwriteConfig(config, filename).removeStaleFiles) { ++ const staleFiles = previouslyGeneratedFiles.filter(f => !filenames.includes(f.filename)); ++ for (const staleFile of staleFiles) { ++ const { filename } = staleFile; ++ if (normalizeOverwriteConfig(config, staleFile).removeStaleFiles) { + (0, file_system_js_1.unlinkFile)(filename, err => { + const prettyFilename = filename.replace(`${input.cwd || process.cwd()}/`, ''); + if (err) { +@@ -35,7 +36,12 @@ async function generate(input, saveToFile = true) { + }); + } + } +- previouslyGeneratedFilenames = filenames; ++ // Keep each file's own `overwrite` around so a file that disappears in a ++ // later run is still judged by the `generates` entry that produced it. ++ previouslyGeneratedFiles = generationResult.map(({ filename, overwrite }) => ({ ++ filename, ++ overwrite, ++ })); + } + const recentOutputHash = new Map(); + async function writeOutput(generationResult) { +@@ -55,7 +61,7 @@ async function generate(input, saveToFile = true) { + if (previousHash) { + recentOutputHash.set(result.filename, previousHash); + } +- if (!normalizeOverwriteConfig(config, result.filename).updateExistingFiles && exists) { ++ if (!normalizeOverwriteConfig(config, result).updateExistingFiles && exists) { + return; + } + let content = result.content || ''; +@@ -132,19 +138,13 @@ async function generate(input, saveToFile = true) { + await writeProfilerOutput(); + return outputFiles; + } +-function normalizeOverwriteConfig(config, outputPath) { +- const overwrite = (function getOverwriteOption() { +- const { overwrite: result = true } = config; - const outputConfig = config.generates[outputPath]; -+ const outputConfig = findOutputConfig(config.generates, outputPath); - if (!outputConfig) { - (0, debugging_js_1.debugLog)(`Couldn't find a config of ${outputPath}`); - return result; -@@ -160,9 +160,30 @@ function normalizeOverwriteConfig(config, outputPath) { +- if (!outputConfig) { +- (0, debugging_js_1.debugLog)(`Couldn't find a config of ${outputPath}`); +- return result; +- } +- if (isConfiguredOutput(outputConfig) && outputConfig.overwrite !== undefined) { +- return outputConfig.overwrite; +- } +- return result; +- })(); ++function normalizeOverwriteConfig(config, fileOutput) { ++ // `fileOutput.overwrite` is carried over from the `generates` entry that ++ // produced this file (see `codegen.js`). Looking it up by output path here ++ // does not work: a preset writes many files underneath a single `generates` ++ // entry keyed by its base output dir, so no `generates` key ever equals the ++ // path of a file the preset generated. ++ const overwrite = fileOutput.overwrite ?? config.overwrite ?? true; + if (overwrite === true) { + return { + removeStaleFiles: true, +@@ -160,9 +160,7 @@ function normalizeOverwriteConfig(config, outputPath) { const { removeStaleFiles = true, updateExistingFiles = true } = overwrite; return { removeStaleFiles, updateExistingFiles }; } -+function findOutputConfig(generates, outputPath) { -+ if (generates[outputPath]) { -+ return generates[outputPath]; -+ } -+ // Presets (e.g. the server preset from @eddeee888/gcg-typescript-resolver-files) -+ // write many files nested under a single `generates` entry keyed by the -+ // preset's base output directory, so an exact match against the per-file -+ // `outputPath` never succeeds. Fall back to the longest `generates` key -+ // that is a path-prefix of `outputPath`. -+ let bestMatch; -+ let bestMatchKeyLength = -1; -+ for (const key of Object.keys(generates)) { -+ const isPrefix = outputPath === key || outputPath.startsWith(`${key}/`); -+ if (isPrefix && key.length > bestMatchKeyLength) { -+ bestMatch = generates[key]; -+ bestMatchKeyLength = key.length; -+ } -+ } -+ return bestMatch; -+} - function isConfiguredOutput(output) { +-function isConfiguredOutput(output) { - return typeof output.plugins !== 'undefined'; -+ return typeof output.plugins !== 'undefined' || typeof output.preset !== 'undefined'; - } +-} +exports.normalizeOverwriteConfig = normalizeOverwriteConfig; async function hashFile(filePath) { try { return hash(await (0, file_system_js_1.readFile)(filePath)); +diff --git a/esm/codegen.js b/esm/codegen.js +index 4b6a854da1fdf7303e2a82908ea6db375ca13e17..125f30b63655cb4a19d943e27047aca6422ad559 100644 +--- a/esm/codegen.js ++++ b/esm/codegen.js +@@ -374,6 +374,11 @@ export async function executeCodegen(input) { + filename: outputArgs.filename, + content: output, + hooks: outputConfig.hooks || {}, ++ // Carry the `generates` entry's `overwrite` on the file ++ // itself. A preset writes many files under one entry keyed ++ // by its base output dir, so the entry cannot be recovered ++ // from the generated file's path later on. ++ overwrite: outputConfig.overwrite, + }); + }; + await context.profiler.run(() => Promise.all(outputs.map(process)), `Codegen: ${filename}`); diff --git a/esm/generate-and-save.js b/esm/generate-and-save.js -index be32dbb79ebe8adaae8f98454d63c70eef4b3ca6..bc8bfbeb4c408ce03e9581509d47069913026965 100644 +index be32dbb79ebe8adaae8f98454d63c70eef4b3ca6..648d7056a05cbcf4b4681df59b555d53457c10b8 100644 --- a/esm/generate-and-save.js +++ b/esm/generate-and-save.js -@@ -131,7 +131,7 @@ export async function generate(input, saveToFile = true) { - function normalizeOverwriteConfig(config, outputPath) { - const overwrite = (function getOverwriteOption() { - const { overwrite: result = true } = config; +@@ -13,13 +13,14 @@ export async function generate(input, saveToFile = true) { + const context = ensureContext(input); + const config = context.getConfig(); + await context.profiler.run(() => lifecycleHooks(config.hooks).afterStart(), 'Lifecycle: afterStart'); +- let previouslyGeneratedFilenames = []; ++ let previouslyGeneratedFiles = []; + function removeStaleFiles(config, generationResult) { + const filenames = generationResult.map(o => o.filename); + // find stale files from previous build which are not present in current build +- const staleFilenames = previouslyGeneratedFilenames.filter(f => !filenames.includes(f)); +- for (const filename of staleFilenames) { +- if (normalizeOverwriteConfig(config, filename).removeStaleFiles) { ++ const staleFiles = previouslyGeneratedFiles.filter(f => !filenames.includes(f.filename)); ++ for (const staleFile of staleFiles) { ++ const { filename } = staleFile; ++ if (normalizeOverwriteConfig(config, staleFile).removeStaleFiles) { + unlinkFile(filename, err => { + const prettyFilename = filename.replace(`${input.cwd || process.cwd()}/`, ''); + if (err) { +@@ -31,7 +32,12 @@ export async function generate(input, saveToFile = true) { + }); + } + } +- previouslyGeneratedFilenames = filenames; ++ // Keep each file's own `overwrite` around so a file that disappears in a ++ // later run is still judged by the `generates` entry that produced it. ++ previouslyGeneratedFiles = generationResult.map(({ filename, overwrite }) => ({ ++ filename, ++ overwrite, ++ })); + } + const recentOutputHash = new Map(); + async function writeOutput(generationResult) { +@@ -51,7 +57,7 @@ export async function generate(input, saveToFile = true) { + if (previousHash) { + recentOutputHash.set(result.filename, previousHash); + } +- if (!normalizeOverwriteConfig(config, result.filename).updateExistingFiles && exists) { ++ if (!normalizeOverwriteConfig(config, result).updateExistingFiles && exists) { + return; + } + let content = result.content || ''; +@@ -128,19 +134,13 @@ export async function generate(input, saveToFile = true) { + await writeProfilerOutput(); + return outputFiles; + } +-function normalizeOverwriteConfig(config, outputPath) { +- const overwrite = (function getOverwriteOption() { +- const { overwrite: result = true } = config; - const outputConfig = config.generates[outputPath]; -+ const outputConfig = findOutputConfig(config.generates, outputPath); - if (!outputConfig) { - debugLog(`Couldn't find a config of ${outputPath}`); - return result; -@@ -156,8 +156,28 @@ function normalizeOverwriteConfig(config, outputPath) { +- if (!outputConfig) { +- debugLog(`Couldn't find a config of ${outputPath}`); +- return result; +- } +- if (isConfiguredOutput(outputConfig) && outputConfig.overwrite !== undefined) { +- return outputConfig.overwrite; +- } +- return result; +- })(); ++function normalizeOverwriteConfig(config, fileOutput) { ++ // `fileOutput.overwrite` is carried over from the `generates` entry that ++ // produced this file (see `codegen.js`). Looking it up by output path here ++ // does not work: a preset writes many files underneath a single `generates` ++ // entry keyed by its base output dir, so no `generates` key ever equals the ++ // path of a file the preset generated. ++ const overwrite = fileOutput.overwrite ?? config.overwrite ?? true; + if (overwrite === true) { + return { + removeStaleFiles: true, +@@ -156,9 +156,6 @@ function normalizeOverwriteConfig(config, outputPath) { const { removeStaleFiles = true, updateExistingFiles = true } = overwrite; return { removeStaleFiles, updateExistingFiles }; } -+function findOutputConfig(generates, outputPath) { -+ if (generates[outputPath]) { -+ return generates[outputPath]; -+ } -+ // Presets (e.g. the server preset from @eddeee888/gcg-typescript-resolver-files) -+ // write many files nested under a single `generates` entry keyed by the -+ // preset's base output directory, so an exact match against the per-file -+ // `outputPath` never succeeds. Fall back to the longest `generates` key -+ // that is a path-prefix of `outputPath`. -+ let bestMatch; -+ let bestMatchKeyLength = -1; -+ for (const key of Object.keys(generates)) { -+ const isPrefix = outputPath === key || outputPath.startsWith(`${key}/`); -+ if (isPrefix && key.length > bestMatchKeyLength) { -+ bestMatch = generates[key]; -+ bestMatchKeyLength = key.length; -+ } -+ } -+ return bestMatch; -+} - function isConfiguredOutput(output) { +-function isConfiguredOutput(output) { - return typeof output.plugins !== 'undefined'; -+ return typeof output.plugins !== 'undefined' || typeof output.preset !== 'undefined'; - } +-} async function hashFile(filePath) { try { -@@ -172,3 +192,4 @@ async function hashFile(filePath) { + return hash(await readFile(filePath)); +@@ -172,3 +169,4 @@ async function hashFile(filePath) { throw err; } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 59307f31..8b5cf3f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -204,7 +204,7 @@ settings: excludeLinksFromLockfile: false patchedDependencies: - '@graphql-codegen/cli@7.3.1': b344808ec016720bb03048ccf854992022c6a9d435ef8223a512fc48843ea8e7 + '@graphql-codegen/cli@7.3.1': 83da0d9cc4f83ed581cc57f776ad2884d843d1e515bfddc34715b3e4fa18c91b importers: @@ -222,7 +222,7 @@ importers: version: 1.0.0(jiti@2.7.0)(typescript@6.0.3) '@eddeee888/nx-graphql-code-generator': specifier: 2.1.0 - version: 2.1.0(@graphql-codegen/cli@7.3.1(patch_hash=b344808ec016720bb03048ccf854992022c6a9d435ef8223a512fc48843ea8e7)(@types/node@22.20.1)(graphql@17.0.2)(typescript@6.0.3))(nx@23.1.1(@swc-node/register@1.12.1(@swc/core@1.15.8(@swc/helpers@0.5.23))(@swc/types@0.1.28)(typescript@6.0.3))(@swc/core@1.15.8(@swc/helpers@0.5.23))) + version: 2.1.0(@graphql-codegen/cli@7.3.1(patch_hash=83da0d9cc4f83ed581cc57f776ad2884d843d1e515bfddc34715b3e4fa18c91b)(@types/node@22.20.1)(graphql@17.0.2)(typescript@6.0.3))(nx@23.1.1(@swc-node/register@1.12.1(@swc/core@1.15.8(@swc/helpers@0.5.23))(@swc/types@0.1.28)(typescript@6.0.3))(@swc/core@1.15.8(@swc/helpers@0.5.23))) '@eslint/eslintrc': specifier: 3.3.1 version: 3.3.1 @@ -231,7 +231,7 @@ importers: version: 7.1.0(graphql@17.0.2) '@graphql-codegen/cli': specifier: 7.3.1 - version: 7.3.1(patch_hash=b344808ec016720bb03048ccf854992022c6a9d435ef8223a512fc48843ea8e7)(@types/node@22.20.1)(graphql@17.0.2)(typescript@6.0.3) + version: 7.3.1(patch_hash=83da0d9cc4f83ed581cc57f776ad2884d843d1e515bfddc34715b3e4fa18c91b)(@types/node@22.20.1)(graphql@17.0.2)(typescript@6.0.3) '@graphql-codegen/near-operation-file-preset': specifier: 5.2.1 version: 5.2.1(graphql@17.0.2) @@ -442,7 +442,7 @@ importers: devDependencies: '@graphql-codegen/cli': specifier: ^7.3.1 - version: 7.3.1(patch_hash=b344808ec016720bb03048ccf854992022c6a9d435ef8223a512fc48843ea8e7)(@types/node@22.20.1)(graphql@17.0.2)(typescript@6.0.3) + version: 7.3.1(patch_hash=83da0d9cc4f83ed581cc57f776ad2884d843d1e515bfddc34715b3e4fa18c91b)(@types/node@22.20.1)(graphql@17.0.2)(typescript@6.0.3) packages/typescript-resolver-files-e2e: devDependencies: @@ -7371,9 +7371,9 @@ snapshots: - supports-color - typescript - '@eddeee888/nx-graphql-code-generator@2.1.0(@graphql-codegen/cli@7.3.1(patch_hash=b344808ec016720bb03048ccf854992022c6a9d435ef8223a512fc48843ea8e7)(@types/node@22.20.1)(graphql@17.0.2)(typescript@6.0.3))(nx@23.1.1(@swc-node/register@1.12.1(@swc/core@1.15.8(@swc/helpers@0.5.23))(@swc/types@0.1.28)(typescript@6.0.3))(@swc/core@1.15.8(@swc/helpers@0.5.23)))': + '@eddeee888/nx-graphql-code-generator@2.1.0(@graphql-codegen/cli@7.3.1(patch_hash=83da0d9cc4f83ed581cc57f776ad2884d843d1e515bfddc34715b3e4fa18c91b)(@types/node@22.20.1)(graphql@17.0.2)(typescript@6.0.3))(nx@23.1.1(@swc-node/register@1.12.1(@swc/core@1.15.8(@swc/helpers@0.5.23))(@swc/types@0.1.28)(typescript@6.0.3))(@swc/core@1.15.8(@swc/helpers@0.5.23)))': dependencies: - '@graphql-codegen/cli': 7.3.1(patch_hash=b344808ec016720bb03048ccf854992022c6a9d435ef8223a512fc48843ea8e7)(@types/node@22.20.1)(graphql@17.0.2)(typescript@6.0.3) + '@graphql-codegen/cli': 7.3.1(patch_hash=83da0d9cc4f83ed581cc57f776ad2884d843d1e515bfddc34715b3e4fa18c91b)(@types/node@22.20.1)(graphql@17.0.2)(typescript@6.0.3) '@nx/devkit': 20.8.4(nx@23.1.1(@swc-node/register@1.12.1(@swc/core@1.15.8(@swc/helpers@0.5.23))(@swc/types@0.1.28)(typescript@6.0.3))(@swc/core@1.15.8(@swc/helpers@0.5.23))) semver: 7.8.5 tslib: 2.8.1 @@ -7590,7 +7590,7 @@ snapshots: graphql: 17.0.2 tslib: 2.8.1 - '@graphql-codegen/cli@7.3.1(patch_hash=b344808ec016720bb03048ccf854992022c6a9d435ef8223a512fc48843ea8e7)(@types/node@22.20.1)(graphql@17.0.2)(typescript@6.0.3)': + '@graphql-codegen/cli@7.3.1(patch_hash=83da0d9cc4f83ed581cc57f776ad2884d843d1e515bfddc34715b3e4fa18c91b)(@types/node@22.20.1)(graphql@17.0.2)(typescript@6.0.3)': dependencies: '@babel/generator': 7.29.8 '@babel/template': 7.29.7