Skip to content

Commit c018381

Browse files
authored
Features/improved before one file write hook (#8662)
* allow beforeOneFileWrite to alter the content * changeset
1 parent 0eb0dde commit c018381

File tree

5 files changed

+96
-24
lines changed

5 files changed

+96
-24
lines changed

.changeset/curvy-pens-watch.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@graphql-codegen/cli': minor
3+
'@graphql-codegen/plugin-helpers': minor
4+
---
5+
6+
the life cycle hook beforeOneFileWrite is now able to modify the generated content

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

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export async function generate(
6868
return;
6969
}
7070

71-
const content = result.content || '';
71+
let content = result.content || '';
7272
const currentHash = hash(content);
7373

7474
if (previousHash && currentHash === previousHash) {
@@ -86,17 +86,29 @@ export async function generate(
8686
return;
8787
}
8888

89-
await lifecycleHooks(result.hooks).beforeOneFileWrite(result.filename);
90-
await lifecycleHooks(config.hooks).beforeOneFileWrite(result.filename);
91-
9289
const absolutePath = isAbsolute(result.filename)
9390
? result.filename
9491
: join(input.cwd || process.cwd(), result.filename);
9592

9693
const basedir = dirname(absolutePath);
9794
await mkdirp(basedir);
9895

99-
await writeFile(absolutePath, content);
96+
content = await lifecycleHooks(result.hooks).beforeOneFileWrite(absolutePath, content);
97+
content = await lifecycleHooks(config.hooks).beforeOneFileWrite(absolutePath, content);
98+
99+
if (content !== result.content) {
100+
result.content = content;
101+
// compare the prettified content with the previous hash
102+
// to compare the content with an existing prettified file
103+
if (hash(content) === previousHash) {
104+
debugLog(`Skipping file (${result.filename}) writing due to indentical hash after prettier...`);
105+
// the modified content is NOT stored in recentOutputHash
106+
// so a diff can already be detected before executing the hook
107+
return;
108+
}
109+
}
110+
111+
await writeFile(absolutePath, result.content);
100112
recentOutputHash.set(result.filename, currentHash);
101113

102114
await lifecycleHooks(result.hooks).afterOneFileWrite(result.filename);

packages/graphql-codegen-cli/src/hooks.ts

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,12 @@ function execShellCommand(cmd: string): Promise<string> {
4141

4242
async function executeHooks(
4343
hookName: string,
44-
_scripts: Types.LifeCycleHookValue = [],
45-
args: string[] = []
46-
): Promise<void> {
44+
_scripts: Types.LifeCycleHookValue | Types.LifeCycleAlterHookValue = [],
45+
args: string[] = [],
46+
initialState?: string
47+
): Promise<void | string> {
4748
debugLog(`Running lifecycle hook "${hookName}" scripts...`);
49+
let state = initialState;
4850
const scripts = Array.isArray(_scripts) ? _scripts : [_scripts];
4951

5052
const quotedArgs = quote(args);
@@ -54,9 +56,16 @@ async function executeHooks(
5456
await execShellCommand(`${script} ${quotedArgs}`);
5557
} else {
5658
debugLog(`Running lifecycle hook "${hookName}" script: ${script.name} with args: ${args.join(' ')}...`);
57-
await script(...args);
59+
const hookArgs = state === undefined ? args : [...args, state];
60+
const hookResult = await script(...hookArgs);
61+
if (typeof hookResult === 'string' && typeof state === 'string') {
62+
debugLog(`Received new content from lifecycle hook "${hookName}" script: ${script.name}`);
63+
state = hookResult;
64+
}
5865
}
5966
}
67+
68+
return state;
6069
}
6170

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

6877
return {
69-
afterStart: async (): Promise<void> => executeHooks('afterStart', hooks.afterStart),
70-
onWatchTriggered: async (event: string, path: string): Promise<void> =>
71-
executeHooks('onWatchTriggered', hooks.onWatchTriggered, [event, path]),
72-
onError: async (error: string): Promise<void> => executeHooks('onError', hooks.onError, [error]),
73-
afterOneFileWrite: async (path: string): Promise<void> =>
74-
executeHooks('afterOneFileWrite', hooks.afterOneFileWrite, [path]),
75-
afterAllFileWrite: async (paths: string[]): Promise<void> =>
76-
executeHooks('afterAllFileWrite', hooks.afterAllFileWrite, paths),
77-
beforeOneFileWrite: async (path: string): Promise<void> =>
78-
executeHooks('beforeOneFileWrite', hooks.beforeOneFileWrite, [path]),
79-
beforeAllFileWrite: async (paths: string[]): Promise<void> =>
80-
executeHooks('beforeAllFileWrite', hooks.beforeAllFileWrite, paths),
81-
beforeDone: async (): Promise<void> => executeHooks('beforeDone', hooks.beforeDone),
78+
afterStart: async (): Promise<void> => {
79+
await executeHooks('afterStart', hooks.afterStart);
80+
},
81+
onWatchTriggered: async (event: string, path: string): Promise<void> => {
82+
await executeHooks('onWatchTriggered', hooks.onWatchTriggered, [event, path]);
83+
},
84+
onError: async (error: string): Promise<void> => {
85+
await executeHooks('onError', hooks.onError, [error]);
86+
},
87+
afterOneFileWrite: async (path: string): Promise<void> => {
88+
await executeHooks('afterOneFileWrite', hooks.afterOneFileWrite, [path]);
89+
},
90+
afterAllFileWrite: async (paths: string[]): Promise<void> => {
91+
await executeHooks('afterAllFileWrite', hooks.afterAllFileWrite, paths);
92+
},
93+
beforeOneFileWrite: async (path: string, content: string): Promise<string> => {
94+
const result = await executeHooks('beforeOneFileWrite', hooks.beforeOneFileWrite, [path], content);
95+
return typeof result === 'string' ? result : content;
96+
},
97+
beforeAllFileWrite: async (paths: string[]): Promise<void> => {
98+
await executeHooks('beforeAllFileWrite', hooks.beforeAllFileWrite, paths);
99+
},
100+
beforeDone: async (): Promise<void> => {
101+
await executeHooks('beforeDone', hooks.beforeDone);
102+
},
82103
};
83104
};

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,4 +213,28 @@ describe('generate-and-save', () => {
213213
// makes sure it doesn't write a new file
214214
expect(writeSpy).toHaveBeenCalled();
215215
});
216+
test('should allow to alter the content with the beforeOneFileWrite hook', async () => {
217+
const filename = 'modify.ts';
218+
const writeSpy = jest.spyOn(fs, 'writeFile').mockImplementation();
219+
220+
const output = await generate(
221+
{
222+
schema: SIMPLE_TEST_SCHEMA,
223+
generates: {
224+
[filename]: {
225+
plugins: ['typescript'],
226+
hooks: {
227+
beforeOneFileWrite: [() => 'new content'],
228+
},
229+
},
230+
},
231+
},
232+
true
233+
);
234+
235+
expect(output.length).toBe(1);
236+
expect(output[0].content).toMatch('new content');
237+
// makes sure it doesn't write a new file
238+
expect(writeSpy).toHaveBeenCalled();
239+
});
216240
});

packages/utils/plugins-helpers/src/types.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -523,8 +523,14 @@ export namespace Types {
523523
export type ComplexPluginOutput = { content: string; prepend?: string[]; append?: string[] };
524524
export type PluginOutput = string | ComplexPluginOutput;
525525
export type HookFunction = (...args: any[]) => void | Promise<void>;
526+
export type HookAlterFunction = (...args: any[]) => void | string | Promise<void | string>;
526527

527528
export type LifeCycleHookValue = string | HookFunction | (string | HookFunction)[];
529+
export type LifeCycleAlterHookValue =
530+
| string
531+
| HookFunction
532+
| HookAlterFunction
533+
| (string | HookFunction | HookAlterFunction)[];
528534

529535
/**
530536
* @description All available lifecycle hooks
@@ -565,11 +571,14 @@ export namespace Types {
565571
*/
566572
afterAllFileWrite: LifeCycleHookValue;
567573
/**
568-
* @description Triggered before a file is written to the file-system. Executed with the path for the file.
574+
* @description Triggered before a file is written to the file-system.
575+
* Executed with the path and content for the file.
576+
*
577+
* Returning a string will override the content of the file.
569578
*
570579
* If the content of the file hasn't changed since last execution - this hooks won't be triggered.
571580
*/
572-
beforeOneFileWrite: LifeCycleHookValue;
581+
beforeOneFileWrite: LifeCycleAlterHookValue;
573582
/**
574583
* @description Executed after the codegen has done creating the output and before writing the files to the file-system.
575584
*

0 commit comments

Comments
 (0)