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

Features/improved before one file write hook #8662

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
6 changes: 6 additions & 0 deletions .changeset/curvy-pens-watch.md
@@ -0,0 +1,6 @@
---
'@graphql-codegen/cli': minor
'@graphql-codegen/plugin-helpers': minor
---

the life cycle hook beforeOneFileWrite is now able to modify the generated content
22 changes: 17 additions & 5 deletions packages/graphql-codegen-cli/src/generate-and-save.ts
Expand Up @@ -68,7 +68,7 @@ export async function generate(
return;
}

const content = result.content || '';
let content = result.content || '';
const currentHash = hash(content);

if (previousHash && currentHash === previousHash) {
Expand All @@ -86,17 +86,29 @@ export async function generate(
return;
}

await lifecycleHooks(result.hooks).beforeOneFileWrite(result.filename);
await lifecycleHooks(config.hooks).beforeOneFileWrite(result.filename);

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

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

await writeFile(absolutePath, content);
content = await lifecycleHooks(result.hooks).beforeOneFileWrite(absolutePath, content);
content = await lifecycleHooks(config.hooks).beforeOneFileWrite(absolutePath, content);

if (content !== result.content) {
result.content = content;
// compare the prettified content with the previous hash
// to compare the content with an existing prettified file
if (hash(content) === previousHash) {
debugLog(`Skipping file (${result.filename}) writing due to indentical hash after prettier...`);
// the modified content is NOT stored in recentOutputHash
// so a diff can already be detected before executing the hook
return;
}
}

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

await lifecycleHooks(result.hooks).afterOneFileWrite(result.filename);
Expand Down
55 changes: 38 additions & 17 deletions packages/graphql-codegen-cli/src/hooks.ts
Expand Up @@ -41,10 +41,12 @@ function execShellCommand(cmd: string): Promise<string> {

async function executeHooks(
hookName: string,
_scripts: Types.LifeCycleHookValue = [],
args: string[] = []
): Promise<void> {
_scripts: Types.LifeCycleHookValue | Types.LifeCycleAlterHookValue = [],
args: string[] = [],
initialState?: string
): Promise<void | string> {
debugLog(`Running lifecycle hook "${hookName}" scripts...`);
let state = initialState;
const scripts = Array.isArray(_scripts) ? _scripts : [_scripts];

const quotedArgs = quote(args);
Expand All @@ -54,9 +56,16 @@ async function executeHooks(
await execShellCommand(`${script} ${quotedArgs}`);
} else {
debugLog(`Running lifecycle hook "${hookName}" script: ${script.name} with args: ${args.join(' ')}...`);
await script(...args);
const hookArgs = state === undefined ? args : [...args, state];
const hookResult = await script(...hookArgs);
if (typeof hookResult === 'string' && typeof state === 'string') {
debugLog(`Received new content from lifecycle hook "${hookName}" script: ${script.name}`);
state = hookResult;
}
}
}

return state;
}

export const lifecycleHooks = (_hooks: Partial<Types.LifecycleHooksDefinition> = {}) => {
Expand All @@ -66,18 +75,30 @@ export const lifecycleHooks = (_hooks: Partial<Types.LifecycleHooksDefinition> =
};

return {
afterStart: async (): Promise<void> => executeHooks('afterStart', hooks.afterStart),
onWatchTriggered: async (event: string, path: string): Promise<void> =>
executeHooks('onWatchTriggered', hooks.onWatchTriggered, [event, path]),
onError: async (error: string): Promise<void> => executeHooks('onError', hooks.onError, [error]),
afterOneFileWrite: async (path: string): Promise<void> =>
executeHooks('afterOneFileWrite', hooks.afterOneFileWrite, [path]),
afterAllFileWrite: async (paths: string[]): Promise<void> =>
executeHooks('afterAllFileWrite', hooks.afterAllFileWrite, paths),
beforeOneFileWrite: async (path: string): Promise<void> =>
executeHooks('beforeOneFileWrite', hooks.beforeOneFileWrite, [path]),
beforeAllFileWrite: async (paths: string[]): Promise<void> =>
executeHooks('beforeAllFileWrite', hooks.beforeAllFileWrite, paths),
beforeDone: async (): Promise<void> => executeHooks('beforeDone', hooks.beforeDone),
afterStart: async (): Promise<void> => {
await executeHooks('afterStart', hooks.afterStart);
},
onWatchTriggered: async (event: string, path: string): Promise<void> => {
await executeHooks('onWatchTriggered', hooks.onWatchTriggered, [event, path]);
},
onError: async (error: string): Promise<void> => {
await executeHooks('onError', hooks.onError, [error]);
},
afterOneFileWrite: async (path: string): Promise<void> => {
await executeHooks('afterOneFileWrite', hooks.afterOneFileWrite, [path]);
},
afterAllFileWrite: async (paths: string[]): Promise<void> => {
await executeHooks('afterAllFileWrite', hooks.afterAllFileWrite, paths);
},
beforeOneFileWrite: async (path: string, content: string): Promise<string> => {
const result = await executeHooks('beforeOneFileWrite', hooks.beforeOneFileWrite, [path], content);
return typeof result === 'string' ? result : content;
},
beforeAllFileWrite: async (paths: string[]): Promise<void> => {
await executeHooks('beforeAllFileWrite', hooks.beforeAllFileWrite, paths);
},
beforeDone: async (): Promise<void> => {
await executeHooks('beforeDone', hooks.beforeDone);
},
};
};
24 changes: 24 additions & 0 deletions packages/graphql-codegen-cli/tests/generate-and-save.spec.ts
Expand Up @@ -213,4 +213,28 @@ describe('generate-and-save', () => {
// makes sure it doesn't write a new file
expect(writeSpy).toHaveBeenCalled();
});
test('should allow to alter the content with the beforeOneFileWrite hook', async () => {
const filename = 'modify.ts';
const writeSpy = jest.spyOn(fs, 'writeFile').mockImplementation();

const output = await generate(
{
schema: SIMPLE_TEST_SCHEMA,
generates: {
[filename]: {
plugins: ['typescript'],
hooks: {
beforeOneFileWrite: [() => 'new content'],
},
},
},
},
true
);

expect(output.length).toBe(1);
expect(output[0].content).toMatch('new content');
// makes sure it doesn't write a new file
expect(writeSpy).toHaveBeenCalled();
});
});
13 changes: 11 additions & 2 deletions packages/utils/plugins-helpers/src/types.ts
Expand Up @@ -523,8 +523,14 @@ export namespace Types {
export type ComplexPluginOutput = { content: string; prepend?: string[]; append?: string[] };
export type PluginOutput = string | ComplexPluginOutput;
export type HookFunction = (...args: any[]) => void | Promise<void>;
export type HookAlterFunction = (...args: any[]) => void | string | Promise<void | string>;

export type LifeCycleHookValue = string | HookFunction | (string | HookFunction)[];
export type LifeCycleAlterHookValue =
| string
| HookFunction
| HookAlterFunction
| (string | HookFunction | HookAlterFunction)[];

/**
* @description All available lifecycle hooks
Expand Down Expand Up @@ -565,11 +571,14 @@ export namespace Types {
*/
afterAllFileWrite: LifeCycleHookValue;
/**
* @description Triggered before a file is written to the file-system. Executed with the path for the file.
* @description Triggered before a file is written to the file-system.
* Executed with the path and content for the file.
*
* Returning a string will override the content of the file.
*
* If the content of the file hasn't changed since last execution - this hooks won't be triggered.
*/
beforeOneFileWrite: LifeCycleHookValue;
beforeOneFileWrite: LifeCycleAlterHookValue;
/**
* @description Executed after the codegen has done creating the output and before writing the files to the file-system.
*
Expand Down