Skip to content

Commit

Permalink
experimentalEnum config (#41)
Browse files Browse the repository at this point in the history
## Summary:
Adds the ability to replace the default string literal union types with flow enums, which are exported for use within the application.

Issue: LP-11757

## Test plan:
Thoroughly unit tested. Add `graphql-flow.config.js` with `experimentalEnums` enabled to test.

Author: nedredmond

Reviewers: jaredly, nedredmond

Required Reviewers:

Approved By: jaredly

Checks: ⌛ Lint & Test (ubuntu-latest, 16.x)

Pull Request URL: #41
  • Loading branch information
nedredmond authored May 25, 2022
1 parent 5078624 commit 093fa5f
Show file tree
Hide file tree
Showing 10 changed files with 199 additions and 57 deletions.
5 changes: 5 additions & 0 deletions .changeset/ninety-geese-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@khanacademy/graphql-flow': minor
---

Enable `experimentalEnums` in a config or subconfig file in to enable the export of flow enum types, which replace the default string union literals. The type currently comes with eslint decorators to skirt a bug in eslint and flow.
1 change: 1 addition & 0 deletions .flowconfig
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ flow-typed/
[lints]

[options]
enums=true

[strict]
5 changes: 5 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ type Options = {
// A template for the name of generated files
// default: [operationName].js
typeFileName?: string,

// Generate flow enums to replace literal unions in generated types. Exports
// each set of enums from each file regardless of other options. Designated
// "experimental" because of bug in eslint that requires config comments.
experimentalEnums?: boolean,
}
```
Expand Down
20 changes: 17 additions & 3 deletions flow-typed/npm/@babel/types_vx.x.x.js
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,23 @@ declare module '@babel/types' {
params: Array<BabelNodeFlowType>,
): BabelNodeTypeParameterInstantiation;

declare function enumDeclaration(
id: BabelNodeIdentifier,
body:
| BabelNodeEnumBooleanBody
| BabelNodeEnumNumberBody
| BabelNodeEnumStringBody
| BabelNodeEnumSymbolBody,
): BabelNodeEnumDeclaration;
declare function enumStringBody(
members: Array<
BabelNodeEnumStringMember | BabelNodeEnumDefaultedMember,
>,
): BabelNodeEnumStringBody;
declare function enumDefaultedMember(
id: BabelNodeIdentifier,
): BabelNodeEnumDefaultedMember;

/*
NOTE(jared): There's something weird in the following couple hundred lines
that makes flow ignore this whole file 🙃.
Expand Down Expand Up @@ -662,9 +679,6 @@ declare module '@babel/types' {
id: BabelNodeIdentifier,
init: BabelNodeStringLiteral,
): BabelNodeEnumStringMember;
declare function enumDefaultedMember(
id: BabelNodeIdentifier,
): BabelNodeEnumDefaultedMember;
declare function jsxAttribute(
name: BabelNodeJSXIdentifier | BabelNodeJSXNamespacedName,
value?:
Expand Down
52 changes: 52 additions & 0 deletions src/__test__/generateTypeFileContents.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,56 @@ describe('generateTypeFileContents', () => {
"
`);
});

describe('experimentalEnums', () => {
it('should generate the expected values', () => {
const {files} = generateTypeFileContents(
'hello.js',
exampleSchema,
gql`
query Hello {
human(id: "Han") {
appearsIn
}
}
`,
{
experimentalEnums: true,
},
'__generated__',
indexPrelude('yarn queries'),
);
expect(
Object.keys(files)
.map((k) => `// ${k}\n${files[k]}`)
.join('\n\n'),
).toMatchInlineSnapshot(`
"// __generated__/Hello.js
// @flow
// AUTOGENERATED -- DO NOT EDIT
// Generated for operation 'Hello' in file '../hello.js'
export type HelloType = {|
variables: {||},
response: {|
/** A human character*/
human: ?{|
appearsIn: ?$ReadOnlyArray<
/** - NEW_HOPE
- EMPIRE
- JEDI*/
?Episode>
|}
|}
|};
/* eslint-disable no-undef */
export enum Episode {
NEW_HOPE,
EMPIRE,
JEDI,
};
/* eslint-enable no-undef */
"
`);
});
});
});
1 change: 1 addition & 0 deletions src/cli/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ const validateOptions = (
'generatedDirectory',
'exportAllObjectTypes',
'typeFileName',
'experimentalEnums',
];
Object.keys(options).forEach((k) => {
if (!externalOptionsKeys.includes(k)) {
Expand Down
47 changes: 38 additions & 9 deletions src/enums.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,35 @@
* Both input & output types can have enums & scalars.
*/
import * as babelTypes from '@babel/types';
import {type BabelNodeFlowType} from '@babel/types';
import type {BabelNodeFlowType} from '@babel/types';
import type {Config} from './types';
import {maybeAddDescriptionComment} from './utils';
import type {IntrospectionEnumType} from 'graphql/utilities/introspectionQuery';

export const experimentalEnumTypeToFlow = (
config: Config,
enumConfig: IntrospectionEnumType,
description: string,
): BabelNodeFlowType => {
const enumDeclaration = babelTypes.enumDeclaration(
// pass id into generic type annotation
babelTypes.identifier(enumConfig.name),
babelTypes.enumStringBody(
enumConfig.enumValues.map((v) =>
babelTypes.enumDefaultedMember(babelTypes.identifier(v.name)),
),
),
);

if (config.experimentalEnumsMap) {
config.experimentalEnumsMap[enumConfig.name] = enumDeclaration;
}

return maybeAddDescriptionComment(
description,
babelTypes.genericTypeAnnotation(enumDeclaration.id),
);
};

export const enumTypeToFlow = (
config: Config,
Expand All @@ -25,14 +51,17 @@ export const enumTypeToFlow = (
combinedDescription =
enumConfig.description + '\n\n' + combinedDescription;
}
return maybeAddDescriptionComment(
combinedDescription,
babelTypes.unionTypeAnnotation(
enumConfig.enumValues.map((n) =>
babelTypes.stringLiteralTypeAnnotation(n.name),
),
),
);

return config.experimentalEnumsMap
? experimentalEnumTypeToFlow(config, enumConfig, combinedDescription)
: maybeAddDescriptionComment(
combinedDescription,
babelTypes.unionTypeAnnotation(
enumConfig.enumValues.map((n) =>
babelTypes.stringLiteralTypeAnnotation(n.name),
),
),
);
};

export const builtinScalars: {[key: string]: string} = {
Expand Down
84 changes: 50 additions & 34 deletions src/generateTypeFiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type ExternalOptions = {
generatedDirectory?: string,
exportAllObjectTypes?: boolean,
typeFileName?: string,
experimentalEnums?: boolean,
};

export const indexPrelude = (regenerateCommand?: string): string => `// @flow
Expand Down Expand Up @@ -62,40 +63,54 @@ export const generateTypeFileContents = (
};

const generated = documentToFlowTypes(document, schema, options);
generated.forEach(({name, typeName, code, isFragment, extraTypes}) => {
// We write all generated files to a `__generated__` subdir to keep
// things tidy.
const targetFileName = options.typeFileName
? options.typeFileName.replace('[operationName]', name)
: `${name}.js`;
const targetPath = path.join(generatedDir, targetFileName);

let fileContents =
'// @' +
`flow\n// AUTOGENERATED -- DO NOT EDIT\n` +
`// Generated for operation '${name}' in file '../${path.basename(
fileName,
)}'\n` +
(options.regenerateCommand
? `// To regenerate, run '${options.regenerateCommand}'.\n`
: '') +
code;
if (options.splitTypes && !isFragment) {
fileContents +=
`\nexport type ${name} = ${typeName}['response'];\n` +
`export type ${name}Variables = ${typeName}['variables'];\n`;
}
Object.keys(extraTypes).forEach((name) => {
fileContents += `\n\nexport type ${name} = ${extraTypes[name]};`;
});

addToIndex(targetPath, typeName);
files[targetPath] =
fileContents
// Remove whitespace from the ends of lines; babel's generate sometimes
// leaves them hanging around.
.replace(/\s+$/gm, '') + '\n';
});
generated.forEach(
({name, typeName, code, isFragment, extraTypes, experimentalEnums}) => {
// We write all generated files to a `__generated__` subdir to keep
// things tidy.
const targetFileName = options.typeFileName
? options.typeFileName.replace('[operationName]', name)
: `${name}.js`;
const targetPath = path.join(generatedDir, targetFileName);

let fileContents =
'// @' +
`flow\n// AUTOGENERATED -- DO NOT EDIT\n` +
`// Generated for operation '${name}' in file '../${path.basename(
fileName,
)}'\n` +
(options.regenerateCommand
? `// To regenerate, run '${options.regenerateCommand}'.\n`
: '') +
code;
if (options.splitTypes && !isFragment) {
fileContents +=
`\nexport type ${name} = ${typeName}['response'];\n` +
`export type ${name}Variables = ${typeName}['variables'];\n`;
}
Object.keys(extraTypes).forEach(
(name) =>
(fileContents += `\n\nexport type ${name} = ${extraTypes[name]};`),
);
const enumNames = Object.keys(experimentalEnums);
if (options.experimentalEnums && enumNames.length) {
// TODO(somewhatabstract, FEI-4172): Update to fixed eslint-plugin-flowtype
// and remove this disable.
fileContents += `\n\n/* eslint-disable no-undef */`;
enumNames.forEach(
(name) =>
(fileContents += `\nexport ${experimentalEnums[name]};\n`),
);
fileContents += `/* eslint-enable no-undef */`;
}

addToIndex(targetPath, typeName);
files[targetPath] =
fileContents
// Remove whitespace from the ends of lines; babel's generate sometimes
// leaves them hanging around.
.replace(/\s+$/gm, '') + '\n';
},
);

return {files, indexContents};
};
Expand Down Expand Up @@ -178,6 +193,7 @@ export const processPragmas = (
generatedDirectory: options.generatedDirectory,
exportAllObjectTypes: options.exportAllObjectTypes,
typeFileName: options.typeFileName,
experimentalEnums: options.experimentalEnums,
};
} else {
return null;
Expand Down
38 changes: 27 additions & 11 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
generateResponseType,
} from './generateResponseType';
import {generateVariablesType} from './generateVariablesType';
import type {BabelNode} from '@babel/types';
export {spyOnGraphqlTagToCollectQueries} from './jest-mock-graphql-tag';

import type {Config, Options, Schema} from './types';
Expand Down Expand Up @@ -42,6 +43,7 @@ const optionsToConfig = (
errors,
allObjectTypes: null,
path: [],
experimentalEnumsMap: options?.experimentalEnums ? {} : undefined,
...internalOptions,
};

Expand All @@ -66,6 +68,7 @@ export const documentToFlowTypes = (
code: string,
isFragment?: boolean,
extraTypes: {[key: string]: string},
experimentalEnums: {[key: string]: string},
}> => {
const errors: Array<string> = [];
const config = optionsToConfig(
Expand All @@ -90,17 +93,19 @@ export const documentToFlowTypes = (
: null,
},
)};`;
const extraTypes: {[key: string]: string} = {};
Object.keys(types).forEach((k) => {
// eslint-disable-next-line flowtype-errors/uncovered
extraTypes[k] = generate(types[k]).code;
});

const extraTypes = codegenExtraTypes(types);
const experimentalEnums = codegenExtraTypes(
config.experimentalEnumsMap || {},
);

return {
name,
typeName: name,
code,
isFragment: true,
extraTypes,
experimentalEnums,
};
}
if (
Expand All @@ -127,12 +132,12 @@ export const documentToFlowTypes = (
// We'll see what's required to get webapp on board.
const code = `export type ${typeName} = {|\n variables: ${variables},\n response: ${response}\n|};`;

const extraTypes: {[key: string]: string} = {};
Object.keys(types).forEach((k) => {
// eslint-disable-next-line flowtype-errors/uncovered
extraTypes[k] = generate(types[k]).code;
});
return {name, typeName, code, extraTypes};
const extraTypes = codegenExtraTypes(types);
const experimentalEnums = codegenExtraTypes(
config.experimentalEnumsMap || {},
);

return {name, typeName, code, extraTypes, experimentalEnums};
}
})
.filter(Boolean);
Expand All @@ -141,3 +146,14 @@ export const documentToFlowTypes = (
}
return result;
};

function codegenExtraTypes(types: {[key: string]: BabelNode}): {
[key: string]: string,
} {
const extraTypes: {[key: string]: string} = {};
Object.keys(types).forEach((k: string) => {
// eslint-disable-next-line flowtype-errors/uncovered
extraTypes[k] = generate(types[k]).code;
});
return extraTypes;
}
3 changes: 3 additions & 0 deletions src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type Options = {|
generatedDirectory?: string,
exportAllObjectTypes?: boolean,
typeFileName?: string,
experimentalEnums?: boolean, // default false
|};

export type Schema = {
Expand Down Expand Up @@ -58,5 +59,7 @@ export type Config = {
scalars: Scalars,
errors: Array<string>,
allObjectTypes: null | {[key: string]: BabelNode},

experimentalEnumsMap?: {[key: string]: BabelNode}, // index signature that is populated with declarations
};
export type Scalars = {[key: string]: 'string' | 'number' | 'boolean'};

0 comments on commit 093fa5f

Please sign in to comment.