Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve generation performance #8642

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/odd-files-train.md
@@ -0,0 +1,5 @@
---
'@graphql-codegen/cli': patch
---

faster type generation
1 change: 0 additions & 1 deletion packages/graphql-codegen-cli/package.json
Expand Up @@ -69,7 +69,6 @@
"json-to-pretty-yaml": "^1.2.2",
"listr2": "^4.0.5",
"log-symbols": "^4.0.0",
"mkdirp": "^1.0.4",
"shell-quote": "^1.7.3",
"string-env-interpolation": "^1.0.1",
"ts-log": "^2.2.3",
Expand Down
48 changes: 34 additions & 14 deletions packages/graphql-codegen-cli/src/generate-and-save.ts
Expand Up @@ -2,8 +2,7 @@ import { lifecycleHooks } from './hooks.js';
import { Types } from '@graphql-codegen/plugin-helpers';
import { executeCodegen } from './codegen.js';
import { createWatcher } from './utils/watcher.js';
import { fileExists, readFile, writeFile, unlinkFile } from './utils/file-system.js';
import mkdirp from 'mkdirp';
import { readFile, writeFile, unlinkFile, mkdirp } from './utils/file-system.js';
import { dirname, join, isAbsolute } from 'path';
import { debugLog } from './utils/debugging.js';
import { CodegenContext, ensureContext } from './config.js';
Expand Down Expand Up @@ -57,41 +56,49 @@ export async function generate(
() =>
Promise.all(
generationResult.map(async (result: Types.FileOutput) => {
const exists = await fileExists(result.filename);
const previousHash = recentOutputHash.get(result.filename) || (await hashFile(result.filename));
const exists = previousHash !== null;

// Store previous hash to avoid reading from disk again
if (previousHash) {
recentOutputHash.set(result.filename, previousHash);
}

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

const content = result.content || '';
const currentHash = hash(content);
let previousHash = recentOutputHash.get(result.filename);

if (!previousHash && exists) {
previousHash = hash(await readFile(result.filename));
}

if (previousHash && currentHash === previousHash) {
debugLog(`Skipping file (${result.filename}) writing due to indentical hash...`);
return;
} else if (context.checkMode) {
}

// skip updating file in dry mode
if (context.checkMode) {
context.checkModeStaleFiles.push(result.filename);
return; // skip updating file in dry mode
return;
}

if (content.length === 0) {
return;
}

recentOutputHash.set(result.filename, currentHash);
const basedir = dirname(result.filename);
await lifecycleHooks(result.hooks).beforeOneFileWrite(result.filename);
await lifecycleHooks(config.hooks).beforeOneFileWrite(result.filename);
await mkdirp(basedir);

const absolutePath = isAbsolute(result.filename)
? result.filename
: join(input.cwd || process.cwd(), result.filename);
await writeFile(absolutePath, result.content);

const basedir = dirname(absolutePath);
await mkdirp(basedir);

await writeFile(absolutePath, content);
recentOutputHash.set(result.filename, currentHash);

await lifecycleHooks(result.hooks).afterOneFileWrite(result.filename);
await lifecycleHooks(config.hooks).afterOneFileWrite(result.filename);
})
Expand Down Expand Up @@ -143,3 +150,16 @@ function shouldOverwrite(config: Types.Config, outputPath: string): boolean {
function isConfiguredOutput(output: any): output is Types.ConfiguredOutput {
return typeof output.plugins !== 'undefined';
}

async function hashFile(filePath: string): Promise<string | null> {
try {
return hash(await readFile(filePath));
} catch (err) {
if (err && err.code === 'ENOENT') {
// return null if file does not exist
return null;
}
// rethrow unexpected errors
throw err;
}
}
14 changes: 5 additions & 9 deletions packages/graphql-codegen-cli/src/utils/file-system.ts
@@ -1,5 +1,5 @@
import { unlink as fsUnlink, promises } from 'fs';
const { writeFile: fsWriteFile, readFile: fsReadFile, stat: fsStat } = promises;
const { writeFile: fsWriteFile, readFile: fsReadFile, mkdir } = promises;

export function writeFile(filepath: string, content: string) {
return fsWriteFile(filepath, content);
Expand All @@ -9,14 +9,10 @@ export function readFile(filepath: string) {
return fsReadFile(filepath, 'utf-8');
}

export async function fileExists(filePath: string): Promise<boolean> {
try {
return (await fsStat(filePath)).isFile();
} catch (err) {
return false;
}
}

export function unlinkFile(filePath: string, cb?: (err?: Error) => any): void {
fsUnlink(filePath, cb);
}

export function mkdirp(filePath: string) {
return mkdir(filePath, { recursive: true });
}
15 changes: 6 additions & 9 deletions packages/graphql-codegen-cli/tests/generate-and-save.spec.ts
Expand Up @@ -49,8 +49,8 @@ describe('generate-and-save', () => {
const filename = 'overwrite.ts';
const writeSpy = jest.spyOn(fs, 'writeFile').mockImplementation();
// forces file to exist
const fileExistsSpy = jest.spyOn(fs, 'fileExists');
fileExistsSpy.mockImplementation(async file => file === filename);
const fileReadSpy = jest.spyOn(fs, 'readFile');
fileReadSpy.mockImplementation(async () => '');

const output = await generate(
{
Expand All @@ -71,7 +71,7 @@ describe('generate-and-save', () => {

expect(output.length).toBe(1);
// makes sure it checks if file is there
expect(fileExistsSpy).toHaveBeenCalledWith(filename);
expect(fileReadSpy).toHaveBeenCalledWith(filename);
// makes sure it doesn't write a new file
expect(writeSpy).not.toHaveBeenCalled();
});
Expand Down Expand Up @@ -105,8 +105,8 @@ describe('generate-and-save', () => {
const filename = 'overwrite.ts';
const writeSpy = jest.spyOn(fs, 'writeFile').mockImplementation();
// forces file to exist
const fileExistsSpy = jest.spyOn(fs, 'fileExists');
fileExistsSpy.mockImplementation(async file => file === filename);
const fileReadSpy = jest.spyOn(fs, 'readFile');
fileReadSpy.mockImplementation(async () => '');

const output = await generate(
{
Expand All @@ -126,7 +126,7 @@ describe('generate-and-save', () => {

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

const output = await generate(
{
Expand Down