Skip to content
Closed
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
3 changes: 3 additions & 0 deletions packages/typescript-resolver-files/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@
"ts-morph": "^22.0.0",
"tslib": "^2.8.0"
},
"devDependencies": {
"@graphql-codegen/cli": "^7.3.1"
},
"files": [
"dist",
"!**/*.tsbuildinfo"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { createRequire } from 'module';
import * as path from 'path';
import { pathToFileURL } from 'url';
import { executeCodegen } from '@graphql-codegen/cli';

/**
* `@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.
*
* 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.
*
* 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<string, unknown>) =>
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 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 },
fileOutput: { filename: string; overwrite?: unknown }
) => { removeStaleFiles: boolean; updateExistingFiles: boolean };
}

describe('normalizeOverwriteConfig()', () => {
it('uses the overwrite carried on the generated file', async () => {
const normalizeOverwriteConfig = await loadNormalizeOverwriteConfig();

expect(
normalizeOverwriteConfig(
{ overwrite: true },
{
filename: `${baseOutputDir}/resolvers/Query/user.ts`,
overwrite: serverPresetOverwrite,
}
)
).toEqual(serverPresetOverwrite);
});

it('falls back to the global overwrite when the file carries none', async () => {
const normalizeOverwriteConfig = await loadNormalizeOverwriteConfig();

expect(
normalizeOverwriteConfig(
{ overwrite: { removeStaleFiles: false } },
{ filename: 'src/generated.ts' }
)
).toEqual({ removeStaleFiles: false, updateExistingFiles: true });
});

it('expands the boolean shorthand', async () => {
const normalizeOverwriteConfig = await loadNormalizeOverwriteConfig();

expect(
normalizeOverwriteConfig({}, { filename: 'a.ts', overwrite: false })
).toEqual({ removeStaleFiles: false, updateExistingFiles: false });

expect(normalizeOverwriteConfig({}, { filename: 'a.ts' })).toEqual({
removeStaleFiles: true,
updateExistingFiles: true,
});
});
});
204 changes: 204 additions & 0 deletions patches/@graphql-codegen__cli@7.3.1.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
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..3ec481027a1ba66d0dc3054498ee09d833b6eea3 100644
--- a/cjs/generate-and-save.js
+++ b/cjs/generate-and-save.js
@@ -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];
- 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 isConfiguredOutput(output) {
- return typeof output.plugins !== '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..648d7056a05cbcf4b4681df59b555d53457c10b8 100644
--- a/esm/generate-and-save.js
+++ b/esm/generate-and-save.js
@@ -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];
- 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 isConfiguredOutput(output) {
- return typeof output.plugins !== 'undefined';
-}
async function hashFile(filePath) {
try {
return hash(await readFile(filePath));
@@ -172,3 +169,4 @@ async function hashFile(filePath) {
throw err;
}
}
+export { normalizeOverwriteConfig };
Loading
Loading