From 7cae52f271573045f5273deec7d2ca82cd4df0b1 Mon Sep 17 00:00:00 2001 From: Jan Nicklas Date: Tue, 29 Nov 2022 19:51:58 +0100 Subject: [PATCH 1/2] allow beforeOneFileWrite to alter the content --- .../src/generate-and-save.ts | 22 ++++++-- packages/graphql-codegen-cli/src/hooks.ts | 55 +++++++++++++------ .../tests/generate-and-save.spec.ts | 24 ++++++++ packages/utils/plugins-helpers/src/types.ts | 13 ++++- 4 files changed, 90 insertions(+), 24 deletions(-) diff --git a/packages/graphql-codegen-cli/src/generate-and-save.ts b/packages/graphql-codegen-cli/src/generate-and-save.ts index 7da85eaed51..11ca65e2af4 100644 --- a/packages/graphql-codegen-cli/src/generate-and-save.ts +++ b/packages/graphql-codegen-cli/src/generate-and-save.ts @@ -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) { @@ -86,9 +86,6 @@ 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); @@ -96,7 +93,22 @@ export async function generate( 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); diff --git a/packages/graphql-codegen-cli/src/hooks.ts b/packages/graphql-codegen-cli/src/hooks.ts index 8710988a31a..be9059a943e 100644 --- a/packages/graphql-codegen-cli/src/hooks.ts +++ b/packages/graphql-codegen-cli/src/hooks.ts @@ -41,10 +41,12 @@ function execShellCommand(cmd: string): Promise { async function executeHooks( hookName: string, - _scripts: Types.LifeCycleHookValue = [], - args: string[] = [] -): Promise { + _scripts: Types.LifeCycleHookValue | Types.LifeCycleAlterHookValue = [], + args: string[] = [], + initialState?: string +): Promise { debugLog(`Running lifecycle hook "${hookName}" scripts...`); + let state = initialState; const scripts = Array.isArray(_scripts) ? _scripts : [_scripts]; const quotedArgs = quote(args); @@ -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 = {}) => { @@ -66,18 +75,30 @@ export const lifecycleHooks = (_hooks: Partial = }; return { - afterStart: async (): Promise => executeHooks('afterStart', hooks.afterStart), - onWatchTriggered: async (event: string, path: string): Promise => - executeHooks('onWatchTriggered', hooks.onWatchTriggered, [event, path]), - onError: async (error: string): Promise => executeHooks('onError', hooks.onError, [error]), - afterOneFileWrite: async (path: string): Promise => - executeHooks('afterOneFileWrite', hooks.afterOneFileWrite, [path]), - afterAllFileWrite: async (paths: string[]): Promise => - executeHooks('afterAllFileWrite', hooks.afterAllFileWrite, paths), - beforeOneFileWrite: async (path: string): Promise => - executeHooks('beforeOneFileWrite', hooks.beforeOneFileWrite, [path]), - beforeAllFileWrite: async (paths: string[]): Promise => - executeHooks('beforeAllFileWrite', hooks.beforeAllFileWrite, paths), - beforeDone: async (): Promise => executeHooks('beforeDone', hooks.beforeDone), + afterStart: async (): Promise => { + await executeHooks('afterStart', hooks.afterStart); + }, + onWatchTriggered: async (event: string, path: string): Promise => { + await executeHooks('onWatchTriggered', hooks.onWatchTriggered, [event, path]); + }, + onError: async (error: string): Promise => { + await executeHooks('onError', hooks.onError, [error]); + }, + afterOneFileWrite: async (path: string): Promise => { + await executeHooks('afterOneFileWrite', hooks.afterOneFileWrite, [path]); + }, + afterAllFileWrite: async (paths: string[]): Promise => { + await executeHooks('afterAllFileWrite', hooks.afterAllFileWrite, paths); + }, + beforeOneFileWrite: async (path: string, content: string): Promise => { + const result = await executeHooks('beforeOneFileWrite', hooks.beforeOneFileWrite, [path], content); + return typeof result === 'string' ? result : content; + }, + beforeAllFileWrite: async (paths: string[]): Promise => { + await executeHooks('beforeAllFileWrite', hooks.beforeAllFileWrite, paths); + }, + beforeDone: async (): Promise => { + await executeHooks('beforeDone', hooks.beforeDone); + }, }; }; diff --git a/packages/graphql-codegen-cli/tests/generate-and-save.spec.ts b/packages/graphql-codegen-cli/tests/generate-and-save.spec.ts index f0181271ce6..ab156d43780 100644 --- a/packages/graphql-codegen-cli/tests/generate-and-save.spec.ts +++ b/packages/graphql-codegen-cli/tests/generate-and-save.spec.ts @@ -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(); + }); }); diff --git a/packages/utils/plugins-helpers/src/types.ts b/packages/utils/plugins-helpers/src/types.ts index 74e90c61707..9deb94c5bab 100644 --- a/packages/utils/plugins-helpers/src/types.ts +++ b/packages/utils/plugins-helpers/src/types.ts @@ -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; + export type HookAlterFunction = (...args: any[]) => void | string | Promise; export type LifeCycleHookValue = string | HookFunction | (string | HookFunction)[]; + export type LifeCycleAlterHookValue = + | string + | HookFunction + | HookAlterFunction + | (string | HookFunction | HookAlterFunction)[]; /** * @description All available lifecycle hooks @@ -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. * From 0311fc0d563b535b9bb9ad466e2541ac8615851d Mon Sep 17 00:00:00 2001 From: Jan Nicklas Date: Tue, 29 Nov 2022 20:41:04 +0100 Subject: [PATCH 2/2] changeset --- .changeset/curvy-pens-watch.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/curvy-pens-watch.md diff --git a/.changeset/curvy-pens-watch.md b/.changeset/curvy-pens-watch.md new file mode 100644 index 00000000000..6f332db0a23 --- /dev/null +++ b/.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