Skip to content

POC: overwrite.removeStaleFiles ignored for server preset (upstream @graphql-codegen/cli bug) - #488

Draft
eddeee888 wants to merge 2 commits into
masterfrom
claude/checkstalefiles-server-preset-f29nam
Draft

POC: overwrite.removeStaleFiles ignored for server preset (upstream @graphql-codegen/cli bug)#488
eddeee888 wants to merge 2 commits into
masterfrom
claude/checkstalefiles-server-preset-f29nam

Conversation

@eddeee888

@eddeee888 eddeee888 commented Aug 31, 2026

Copy link
Copy Markdown
Owner

The issue

The server preset hard-codes overwrite.removeStaleFiles: false (packages/typescript-resolver-files/src/defineConfig.ts) so that watch mode doesn't delete resolver files. That setting never takes effect.

The cause is in @graphql-codegen/cli@7.3.1, generate-and-save.js:

function normalizeOverwriteConfig(config, outputPath) {
  const overwrite = (function getOverwriteOption() {
    const { overwrite: result = true } = config;
    const outputConfig = config.generates[outputPath];   // outputPath = a generated FILE's path
    if (!outputConfig) {
      debugLog(`Couldn't find a config of ${outputPath}`);
      return result;                                      // <- always taken for presets
    }
    if (isConfiguredOutput(outputConfig) && outputConfig.overwrite !== undefined) {
      return outputConfig.overwrite;
    }
    return result;
  })();
  ...
}

function isConfiguredOutput(output) {
  return typeof output.plugins !== 'undefined';
}

Two separate assumptions break for a preset-based output:

  1. The lookup key is wrong. It's called as normalizeOverwriteConfig(config, filename) for each generated file, but a preset's generates entry is keyed by its baseOutputDir. No generates key ever equals the path of a file the preset emitted, so the lookup always misses.
  2. isConfiguredOutput requires plugins. Even on a hit, a preset entry has no plugins key — the preset supplies plugins internally — so it isn't recognised as a configured output.

Both paths return result, the global config.overwrite, which defaults to trueremoveStaleFiles: true. In watch mode that deletes the very resolver files the preset's false was meant to protect.

updateExistingFiles: true is dropped identically; it just happens to match the fallback default, so only removeStaleFiles is observable.

For the record, overwrite.checkStaleFiles is not a real option at all — NormalizedOverwriteOption in @graphql-codegen/plugin-helpers has exactly removeStaleFiles and updateExistingFiles. And this is not something @eddeee888/gcg-server-config can influence: it produces plugin config, while overwrite is an output-level field.

The patch

patches/@graphql-codegen__cli@7.3.1.patch, applied via pnpm patch / patchedDependencies.

codegen.js already copies hooks from the output config onto every generated file. Its process function has both the generates key and the per-file path in scope, so overwrite can ride along the same way:

result.push({
  filename: outputArgs.filename,
  content: output,
  hooks: outputConfig.hooks || {},
  overwrite: outputConfig.overwrite,   // new
});

normalizeOverwriteConfig then reads the file's own value and needs no lookup:

const overwrite = fileOutput.overwrite ?? config.overwrite ?? true;

That deletes the config.generates lookup and isConfiguredOutput entirely — the function gets smaller, and both failure modes above disappear at once. removeStaleFiles retains the previous run's { filename, overwrite } pairs so a file that disappears is still judged by the entry that produced it.

Matching generates keys by path-prefix was considered and rejected: besides being guesswork, it's wrong. A preset can emit files that aren't under its baseOutputDir at all — the server preset does, e.g. resolvers/User.ts — and those would still have missed. Tagging at the source has no such failure mode.

Tests are in packages/typescript-resolver-files/src/defineConfig.staleFilesCliBug.spec.ts: one drives the public executeCodegen API and asserts every file a preset emits carries the entry's overwrite; three cover normalizeOverwriteConfig's resolution rules (file value wins, global fallback, boolean shorthand). The propagation test uses a stub preset shaped like the server preset — directory-keyed entry, no plugins key, one emitted path deliberately outside the base dir — so it stays independent of the server preset's internals. All four fail against the unpatched dependency and pass against the patched one; the full suite is green at 129 tests.

This is a PoC — the real fix goes upstream

A pnpm patch only affects this repo's own node_modules. It does not reach consumers of @eddeee888/gcg-typescript-resolver-files, whose own @graphql-codegen/cli install is untouched. This branch exists to pin down the root cause and prove the fix works end to end.

The fix belongs in graphql-code-generator/graphql-code-generator and needs to ship in a new @graphql-codegen/cli release before users benefit. When upstreaming, Types.FileOutput lives in @graphql-codegen/plugin-helpers and will need an optional overwrite?: boolean | Partial<NormalizedOverwriteOption> field — not patched here, since this PoC only changes the CLI's runtime JS.

Draft until we decide whether to keep the patch as an interim measure or go straight upstream.

…odegen/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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QRhgmWtrzQAFoN47ebXrkY
@changeset-bot

changeset-bot Bot commented Aug 31, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: f968b93

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

…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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QRhgmWtrzQAFoN47ebXrkY
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants