Skip to content

Commit 5afa923

Browse files
authored
improve generation performance (#8642)
* improve generation performance * add changeset * rethrow unexpected errors * consider cwd for mkdir
1 parent 506a3bd commit 5afa923

File tree

5 files changed

+50
-33
lines changed

5 files changed

+50
-33
lines changed

.changeset/odd-files-train.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-codegen/cli': patch
3+
---
4+
5+
faster type generation

packages/graphql-codegen-cli/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@
6969
"json-to-pretty-yaml": "^1.2.2",
7070
"listr2": "^4.0.5",
7171
"log-symbols": "^4.0.0",
72-
"mkdirp": "^1.0.4",
7372
"shell-quote": "^1.7.3",
7473
"string-env-interpolation": "^1.0.1",
7574
"ts-log": "^2.2.3",

packages/graphql-codegen-cli/src/generate-and-save.ts

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import { lifecycleHooks } from './hooks.js';
22
import { Types } from '@graphql-codegen/plugin-helpers';
33
import { executeCodegen } from './codegen.js';
44
import { createWatcher } from './utils/watcher.js';
5-
import { fileExists, readFile, writeFile, unlinkFile } from './utils/file-system.js';
6-
import mkdirp from 'mkdirp';
5+
import { readFile, writeFile, unlinkFile, mkdirp } from './utils/file-system.js';
76
import { dirname, join, isAbsolute } from 'path';
87
import { debugLog } from './utils/debugging.js';
98
import { CodegenContext, ensureContext } from './config.js';
@@ -57,41 +56,49 @@ export async function generate(
5756
() =>
5857
Promise.all(
5958
generationResult.map(async (result: Types.FileOutput) => {
60-
const exists = await fileExists(result.filename);
59+
const previousHash = recentOutputHash.get(result.filename) || (await hashFile(result.filename));
60+
const exists = previousHash !== null;
61+
62+
// Store previous hash to avoid reading from disk again
63+
if (previousHash) {
64+
recentOutputHash.set(result.filename, previousHash);
65+
}
6166

6267
if (!shouldOverwrite(config, result.filename) && exists) {
6368
return;
6469
}
6570

6671
const content = result.content || '';
6772
const currentHash = hash(content);
68-
let previousHash = recentOutputHash.get(result.filename);
69-
70-
if (!previousHash && exists) {
71-
previousHash = hash(await readFile(result.filename));
72-
}
7373

7474
if (previousHash && currentHash === previousHash) {
7575
debugLog(`Skipping file (${result.filename}) writing due to indentical hash...`);
7676
return;
77-
} else if (context.checkMode) {
77+
}
78+
79+
// skip updating file in dry mode
80+
if (context.checkMode) {
7881
context.checkModeStaleFiles.push(result.filename);
79-
return; // skip updating file in dry mode
82+
return;
8083
}
8184

8285
if (content.length === 0) {
8386
return;
8487
}
8588

86-
recentOutputHash.set(result.filename, currentHash);
87-
const basedir = dirname(result.filename);
8889
await lifecycleHooks(result.hooks).beforeOneFileWrite(result.filename);
8990
await lifecycleHooks(config.hooks).beforeOneFileWrite(result.filename);
90-
await mkdirp(basedir);
91+
9192
const absolutePath = isAbsolute(result.filename)
9293
? result.filename
9394
: join(input.cwd || process.cwd(), result.filename);
94-
await writeFile(absolutePath, result.content);
95+
96+
const basedir = dirname(absolutePath);
97+
await mkdirp(basedir);
98+
99+
await writeFile(absolutePath, content);
100+
recentOutputHash.set(result.filename, currentHash);
101+
95102
await lifecycleHooks(result.hooks).afterOneFileWrite(result.filename);
96103
await lifecycleHooks(config.hooks).afterOneFileWrite(result.filename);
97104
})
@@ -143,3 +150,16 @@ function shouldOverwrite(config: Types.Config, outputPath: string): boolean {
143150
function isConfiguredOutput(output: any): output is Types.ConfiguredOutput {
144151
return typeof output.plugins !== 'undefined';
145152
}
153+
154+
async function hashFile(filePath: string): Promise<string | null> {
155+
try {
156+
return hash(await readFile(filePath));
157+
} catch (err) {
158+
if (err && err.code === 'ENOENT') {
159+
// return null if file does not exist
160+
return null;
161+
}
162+
// rethrow unexpected errors
163+
throw err;
164+
}
165+
}
Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { unlink as fsUnlink, promises } from 'fs';
2-
const { writeFile: fsWriteFile, readFile: fsReadFile, stat: fsStat } = promises;
2+
const { writeFile: fsWriteFile, readFile: fsReadFile, mkdir } = promises;
33

44
export function writeFile(filepath: string, content: string) {
55
return fsWriteFile(filepath, content);
@@ -9,14 +9,10 @@ export function readFile(filepath: string) {
99
return fsReadFile(filepath, 'utf-8');
1010
}
1111

12-
export async function fileExists(filePath: string): Promise<boolean> {
13-
try {
14-
return (await fsStat(filePath)).isFile();
15-
} catch (err) {
16-
return false;
17-
}
18-
}
19-
2012
export function unlinkFile(filePath: string, cb?: (err?: Error) => any): void {
2113
fsUnlink(filePath, cb);
2214
}
15+
16+
export function mkdirp(filePath: string) {
17+
return mkdir(filePath, { recursive: true });
18+
}

packages/graphql-codegen-cli/tests/generate-and-save.spec.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ describe('generate-and-save', () => {
4949
const filename = 'overwrite.ts';
5050
const writeSpy = jest.spyOn(fs, 'writeFile').mockImplementation();
5151
// forces file to exist
52-
const fileExistsSpy = jest.spyOn(fs, 'fileExists');
53-
fileExistsSpy.mockImplementation(async file => file === filename);
52+
const fileReadSpy = jest.spyOn(fs, 'readFile');
53+
fileReadSpy.mockImplementation(async () => '');
5454

5555
const output = await generate(
5656
{
@@ -71,7 +71,7 @@ describe('generate-and-save', () => {
7171

7272
expect(output.length).toBe(1);
7373
// makes sure it checks if file is there
74-
expect(fileExistsSpy).toHaveBeenCalledWith(filename);
74+
expect(fileReadSpy).toHaveBeenCalledWith(filename);
7575
// makes sure it doesn't write a new file
7676
expect(writeSpy).not.toHaveBeenCalled();
7777
});
@@ -105,8 +105,8 @@ describe('generate-and-save', () => {
105105
const filename = 'overwrite.ts';
106106
const writeSpy = jest.spyOn(fs, 'writeFile').mockImplementation();
107107
// forces file to exist
108-
const fileExistsSpy = jest.spyOn(fs, 'fileExists');
109-
fileExistsSpy.mockImplementation(async file => file === filename);
108+
const fileReadSpy = jest.spyOn(fs, 'readFile');
109+
fileReadSpy.mockImplementation(async () => '');
110110

111111
const output = await generate(
112112
{
@@ -126,7 +126,7 @@ describe('generate-and-save', () => {
126126

127127
expect(output.length).toBe(1);
128128
// makes sure it checks if file is there
129-
expect(fileExistsSpy).toHaveBeenCalledWith(filename);
129+
expect(fileReadSpy).toHaveBeenCalledWith(filename);
130130
// makes sure it doesn't write a new file
131131
expect(writeSpy).not.toHaveBeenCalled();
132132
});
@@ -136,9 +136,6 @@ describe('generate-and-save', () => {
136136
const writeSpy = jest.spyOn(fs, 'writeFile').mockImplementation();
137137
const readSpy = jest.spyOn(fs, 'readFile').mockImplementation();
138138
readSpy.mockImplementation(async _f => '');
139-
// forces file to exist
140-
const fileExistsSpy = jest.spyOn(fs, 'fileExists');
141-
fileExistsSpy.mockImplementation(async file => file === filename);
142139

143140
const output = await generate(
144141
{

0 commit comments

Comments
 (0)