Skip to content

Commit

Permalink
Merge branch 'dotansimha:master' into feature/support-gql-extension
Browse files Browse the repository at this point in the history
  • Loading branch information
danielwaltz authored Dec 7, 2022
2 parents 2fe0bd4 + c018381 commit 9e1a476
Show file tree
Hide file tree
Showing 8 changed files with 226 additions and 24 deletions.
6 changes: 6 additions & 0 deletions .changeset/curvy-pens-watch.md
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions .changeset/perfect-mails-allow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-codegen/client-preset': patch
---

add config for nonOptionalTypename
22 changes: 17 additions & 5 deletions packages/graphql-codegen-cli/src/generate-and-save.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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();
});
});
1 change: 1 addition & 0 deletions packages/presets/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export const preset: Types.OutputPreset<ClientPresetConfig> = {
arrayInputCoercion: options.config.arrayInputCoercion,
enumsAsTypes: options.config.enumsAsTypes,
dedupeFragments: options.config.dedupeFragments,
nonOptionalTypename: options.config.nonOptionalTypename,
};

const visitor = new ClientSideBaseVisitor(options.schemaAst!, [], options.config, options.config);
Expand Down
124 changes: 124 additions & 0 deletions packages/presets/client/tests/client-preset.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,130 @@ export * from "./fragment-masking"`);
);
});

it("follows 'nonOptionalTypename': true", async () => {
const result = await executeCodegen({
schema: [
/* GraphQL */ `
type Query {
a: String
b: String
c: String
}
`,
],
documents: path.join(__dirname, 'fixtures/simple-uppercase-operation-name.ts'),
generates: {
'out1/': {
preset,
plugins: [],
},
},
config: {
nonOptionalTypename: true,
},
});

expect(result.length).toBe(4);
const gqlFile = result.find(file => file.filename === 'out1/gql.ts');
expect(gqlFile.content).toMatchInlineSnapshot(`
"/* eslint-disable */
import * as types from './graphql';
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
/**
* Map of all GraphQL operations in the project.
*
* This map has several performance disadvantages:
* 1. It is not tree-shakeable, so it will include all operations in the project.
* 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
* 3. It does not support dead code elimination, so it will add unused operations.
*
* Therefore it is highly recommended to use the babel-plugin for production.
*/
const documents = {
"\\n query A {\\n a\\n }\\n": types.ADocument,
"\\n query B {\\n b\\n }\\n": types.BDocument,
"\\n fragment C on Query {\\n c\\n }\\n": types.CFragmentDoc,
};
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\\n query A {\\n a\\n }\\n"): (typeof documents)["\\n query A {\\n a\\n }\\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\\n query B {\\n b\\n }\\n"): (typeof documents)["\\n query B {\\n b\\n }\\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\\n fragment C on Query {\\n c\\n }\\n"): (typeof documents)["\\n fragment C on Query {\\n c\\n }\\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*
*
* @example
* \`\`\`ts
* const query = gql(\`query GetUser($id: ID!) { user(id: $id) { name } }\`);
* \`\`\`
*
* The query argument is unknown!
* Please regenerate the types.
**/
export function graphql(source: string): unknown;
export function graphql(source: string) {
return (documents as any)[source] ?? {};
}
export type DocumentType<TDocumentNode extends DocumentNode<any, any>> = TDocumentNode extends DocumentNode< infer TType, any> ? TType : never;"
`);
const graphqlFile = result.find(file => file.filename === 'out1/graphql.ts');
expect(graphqlFile.content).toMatchInlineSnapshot(`
"/* eslint-disable */
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: string;
String: string;
Boolean: boolean;
Int: number;
Float: number;
};
export type Query = {
__typename: 'Query';
a?: Maybe<Scalars['String']>;
b?: Maybe<Scalars['String']>;
c?: Maybe<Scalars['String']>;
};
export type AQueryVariables = Exact<{ [key: string]: never; }>;
export type AQuery = { __typename: 'Query', a?: string | null };
export type BQueryVariables = Exact<{ [key: string]: never; }>;
export type BQuery = { __typename: 'Query', b?: string | null };
export type CFragment = { __typename: 'Query', c?: string | null } & { ' $fragmentName'?: 'CFragment' };
export const CFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"C"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Query"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"c"}}]}}]} as unknown as DocumentNode<CFragment, unknown>;
export const ADocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"A"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"a"}}]}}]} as unknown as DocumentNode<AQuery, AQueryVariables>;
export const BDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"B"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"b"}}]}}]} as unknown as DocumentNode<BQuery, BQueryVariables>;"
`);

expect(graphqlFile.content).toContain("__typename: 'Query';");
});

it('prevent duplicate operations', async () => {
const result = await executeCodegen({
schema: [
Expand Down
13 changes: 11 additions & 2 deletions packages/utils/plugins-helpers/src/types.ts
Original file line number Diff line number Diff line change
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 9e1a476

Please sign in to comment.