Skip to content

Commit

Permalink
Features/improved before one file write hook (#8662)
Browse files Browse the repository at this point in the history
* allow beforeOneFileWrite to alter the content

* changeset
  • Loading branch information
jantimon committed Dec 7, 2022
1 parent 0eb0dde commit c018381
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 24 deletions.
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

0 comments on commit c018381

Please sign in to comment.