From eb64c621ffddcf8452bc98e886da9b5ed60a830c Mon Sep 17 00:00:00 2001 From: Nicolas Beaussart Date: Mon, 23 Oct 2023 15:37:40 +0200 Subject: [PATCH] Add graphql-codegen support (#305) * Add graphql-codegen support * Add `PACKAGE_JSON_PATH` to graphql-codegen * cleenup tests * cleenup logs --------- Co-authored-by: Lars Kappert --- README.md | 2 + fixtures/plugins/graphql-codegen/codegen.ts | 21 +++++++ .../@graphql-codegen/cli/package.json | 3 + fixtures/plugins/graphql-codegen/package.json | 16 ++++++ schema.json | 4 ++ src/ConfigurationValidator.ts | 1 + src/plugins/graphql-codegen/README.md | 22 ++++++++ src/plugins/graphql-codegen/index.ts | 54 ++++++++++++++++++ src/plugins/graphql-codegen/types.ts | 53 ++++++++++++++++++ src/plugins/index.ts | 1 + test/plugins/graphql-codegen.test.ts | 56 +++++++++++++++++++ 11 files changed, 233 insertions(+) create mode 100644 fixtures/plugins/graphql-codegen/codegen.ts create mode 100644 fixtures/plugins/graphql-codegen/node_modules/@graphql-codegen/cli/package.json create mode 100644 fixtures/plugins/graphql-codegen/package.json create mode 100644 src/plugins/graphql-codegen/README.md create mode 100644 src/plugins/graphql-codegen/index.ts create mode 100644 src/plugins/graphql-codegen/types.ts create mode 100644 test/plugins/graphql-codegen.test.ts diff --git a/README.md b/README.md index 6d84c568f..2898ead30 100644 --- a/README.md +++ b/README.md @@ -277,6 +277,7 @@ Knip contains a growing list of plugins: - [ESLint][plugin-eslint] - [Gatsby][plugin-gatsby] - [GitHub Actions][plugin-github-actions] +- [Graphql Codegen][plugin-graphql-codegen] - [husky][plugin-husky] - [Jest][plugin-jest] - [Lefthook][plugin-lefthook] @@ -928,6 +929,7 @@ Special thanks to the wonderful people who have contributed to this project: [plugin-eslint]: ./src/plugins/eslint [plugin-gatsby]: ./src/plugins/gatsby [plugin-github-actions]: ./src/plugins/github-actions +[plugin-graphql-codegen]: ./src/plugins/graphql-codegen [plugin-husky]: ./src/plugins/husky [plugin-jest]: ./src/plugins/jest [plugin-lefthook]: ./src/plugins/lefthook diff --git a/fixtures/plugins/graphql-codegen/codegen.ts b/fixtures/plugins/graphql-codegen/codegen.ts new file mode 100644 index 000000000..7b0b091b7 --- /dev/null +++ b/fixtures/plugins/graphql-codegen/codegen.ts @@ -0,0 +1,21 @@ +module.exports = { + schema: 'schema.graphql', + overwrite: true, + generates: { + './graphql.schema.json': { + plugins: ['introspection'], + }, + './graphql.schema.graphql': { + 'schema-ast': {}, + }, + './src/generated/graphql.ts': { + documents: ['./src/**/*.tsx'], + plugins: ['typescript', 'typescript-operations', 'typescript-urql'], + }, + './lib/': { + documents: ['./lib/**/*.tsx'], + preset: 'near-operation-file-preset', + plugins: ['typescript-operations', 'typescript-msw'], + }, + }, +}; diff --git a/fixtures/plugins/graphql-codegen/node_modules/@graphql-codegen/cli/package.json b/fixtures/plugins/graphql-codegen/node_modules/@graphql-codegen/cli/package.json new file mode 100644 index 000000000..d276c9942 --- /dev/null +++ b/fixtures/plugins/graphql-codegen/node_modules/@graphql-codegen/cli/package.json @@ -0,0 +1,3 @@ +{ + "name": "@graphql-codegen/cli" +} diff --git a/fixtures/plugins/graphql-codegen/package.json b/fixtures/plugins/graphql-codegen/package.json new file mode 100644 index 000000000..56693614a --- /dev/null +++ b/fixtures/plugins/graphql-codegen/package.json @@ -0,0 +1,16 @@ +{ + "name": "@fixtures/_template", + "version": "*", + "devDependencies": { + "@graphql-codegen/cli": "*" + }, + "codegen": { + "schema": "schema.graphql", + "documents": ["src/**/*.tsx", "!src/gql/**/*"], + "generates": { + "./src/gql/": { + "preset": "client" + } + } + } +} diff --git a/schema.json b/schema.json index 2496261ae..4752af07d 100644 --- a/schema.json +++ b/schema.json @@ -276,6 +276,10 @@ "title": "github-actions plugin configuration (https://github.com/webpro/knip/blob/main/src/plugins/github-actions/README.md)", "$ref": "#/definitions/plugin" }, + "graphql-codegen": { + "title": "graphql-codegen plugin configuration (https://github.com/webpro/knip/blob/main/src/plugins/graphql-codegen/README.md)", + "$ref": "#/definitions/plugin" + }, "husky": { "title": "husky plugin configuration (https://github.com/webpro/knip/blob/main/src/plugins/husky/README.md)", "$ref": "#/definitions/plugin" diff --git a/src/ConfigurationValidator.ts b/src/ConfigurationValidator.ts index 878b79810..1a0b98292 100644 --- a/src/ConfigurationValidator.ts +++ b/src/ConfigurationValidator.ts @@ -73,6 +73,7 @@ export const pluginSchema = z.union([ ]); const pluginsSchema = z.object({ + 'graphql-codegen': pluginSchema, astro: pluginSchema, angular: pluginSchema, ava: pluginSchema, diff --git a/src/plugins/graphql-codegen/README.md b/src/plugins/graphql-codegen/README.md new file mode 100644 index 000000000..d65a4f38b --- /dev/null +++ b/src/plugins/graphql-codegen/README.md @@ -0,0 +1,22 @@ +# Graphql Codegen + +## Enabled + +This plugin is enabled when any of the following package names and/or regular expressions has a match in `dependencies` +or `devDependencies`: + +- `/^@graphql-codegen\//` + +## Default configuration + +```json +{ + "graphql-codegen": { + "config": ["codegen.{ts,js,json,yml,mjs,cts}", "package.json"] + } +} +``` + +Also see [Knip plugins][1] for more information about plugins. + +[1]: https://github.com/webpro/knip/blob/main/README.md#plugins diff --git a/src/plugins/graphql-codegen/index.ts b/src/plugins/graphql-codegen/index.ts new file mode 100644 index 000000000..6f529c4fd --- /dev/null +++ b/src/plugins/graphql-codegen/index.ts @@ -0,0 +1,54 @@ +import { timerify } from '../../util/Performance.js'; +import { hasDependency, load } from '../../util/plugin.js'; +import { isConfigurationOutput } from './types.js'; +import type { ConfiguredPlugin, GraphqlCodegenTypes, PresetNames } from './types.js'; +import type { IsPluginEnabledCallback, GenericPluginCallback } from '../../types/plugins.js'; + +export const NAME = 'Graphql Codegen'; + +/** @public */ +export const ENABLERS = [/^@graphql-codegen\//]; + +export const PACKAGE_JSON_PATH = 'codegen'; + +export const isEnabled: IsPluginEnabledCallback = ({ dependencies }) => hasDependency(dependencies, ENABLERS); + +export const CONFIG_FILE_PATTERNS = ['codegen.{ts,js,json,yml,mjs,cts}', 'package.json']; + +const findPluginDependencies: GenericPluginCallback = async (configFilePath, options) => { + const { manifest, isProduction } = options; + + if (isProduction) return []; + + // load configuration file from `configFilePath` (or grab `manifest` for package.json) + // load(FAKE_PATH) will return `undefined` + const localConfig: GraphqlCodegenTypes | undefined = configFilePath.endsWith('package.json') + ? manifest[PACKAGE_JSON_PATH] + : await load(configFilePath); + + if (!localConfig) return []; + + const generateSet = Object.values(localConfig.generates); + + const configurationOutput = generateSet.filter(isConfigurationOutput); + + const presets = configurationOutput + .map(configOutput => (configOutput.preset ? configOutput.preset : undefined)) + .filter((preset): preset is PresetNames => typeof preset === 'string') + .map(presetName => `@graphql-codegen/${presetName}${presetName.endsWith('-preset') ? '' : '-preset'}`); + + const flatPlugins = generateSet + .filter((config): config is ConfiguredPlugin => !isConfigurationOutput(config)) + .map(item => Object.keys(item)) + .flat() + .map(plugin => `@graphql-codegen/${plugin}`); + + const nestedPlugins = configurationOutput + .map(configOutput => (configOutput.plugins ? configOutput.plugins : [])) + .flat() + .map(plugin => `@graphql-codegen/${plugin}`); + + return [...presets, ...flatPlugins, ...nestedPlugins]; +}; + +export const findDependencies = timerify(findPluginDependencies); diff --git a/src/plugins/graphql-codegen/types.ts b/src/plugins/graphql-codegen/types.ts new file mode 100644 index 000000000..f6df4f8d4 --- /dev/null +++ b/src/plugins/graphql-codegen/types.ts @@ -0,0 +1,53 @@ +type PluginConfig = { [key: string]: T }; +export interface ConfiguredPlugin { + [name: string]: PluginConfig; +} +type NamedPlugin = string; + +type OutputConfig = NamedPlugin | ConfiguredPlugin; +type PresetNamesBase = 'client' | 'near-operation-file' | 'gql-tag-operations' | 'graphql-modules' | 'import-types'; +export type PresetNames = `${PresetNamesBase}-preset` | PresetNamesBase; + +type OutputPreset = { + buildGeneratesSection: (options: unknown) => Promise; + prepareDocuments?: (outputFilePath: string, outputSpecificDocuments: unknown) => Promise; +}; + +export function isConfigurationOutput(config: ConfiguredOutput | ConfiguredPlugin[]): config is ConfiguredOutput { + return 'preset' in config || 'plugins' in config; +} + +interface ConfiguredOutput { + /** + * @type array + * @items { "$ref": "#/definitions/GeneratedPluginsMap" } + * @description List of plugins to apply to this current output file. + * + * You can either specify plugins from the community using the NPM package name (after you installed it in your project), or you can use a path to a local file for custom plugins. + * + * You can find a list of available plugins here: https://the-guild.dev/graphql/codegen/docs/plugins/index + * Need a custom plugin? read this: https://the-guild.dev/graphql/codegen/docs/custom-codegen/index + */ + plugins?: OutputConfig[]; + /** + * @description If your setup uses Preset to have a more dynamic setup and output, set the name of your preset here. + * + * Presets are a way to have more than one file output, for example: https://the-guild.dev/graphql/codegen/docs/presets/near-operation-file + * + * You can either specify a preset from the community using the NPM package name (after you installed it in your project), or you can use a path to a local file for a custom preset. + * + * List of available presets: https://graphql-code-generator.com/docs/presets/presets-index + */ + preset?: PresetNames | OutputPreset; +} +// Extracted from https://github.com/dotansimha/graphql-code-generator/blob/master/packages/utils/plugins-helpers/src/types.ts +export interface GraphqlCodegenTypes { + /** + * @description A map where the key represents an output path for the generated code and the value represents a set of options which are relevant for that specific file. + * + * For more details: https://graphql-code-generator.com/docs/config-reference/codegen-config + */ + generates: { + [outputPath: string]: ConfiguredOutput | ConfiguredPlugin[]; + }; +} diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 7bddeade8..24a728544 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -43,3 +43,4 @@ export * as typescript from './typescript/index.js'; export * as vite from './vite/index.js'; export * as vitest from './vitest/index.js'; export * as webpack from './webpack/index.js'; +export * as graphqlCodegen from './graphql-codegen/index.js'; \ No newline at end of file diff --git a/test/plugins/graphql-codegen.test.ts b/test/plugins/graphql-codegen.test.ts new file mode 100644 index 000000000..794d9af23 --- /dev/null +++ b/test/plugins/graphql-codegen.test.ts @@ -0,0 +1,56 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { main } from '../../src/index.js'; +import * as graphqlCodegen from '../../src/plugins/graphql-codegen/index.js'; +import { resolve, join } from '../../src/util/path.js'; +import baseArguments from '../helpers/baseArguments.js'; +import baseCounters from '../helpers/baseCounters.js'; +import { getManifest, pluginConfig as config } from '../helpers/index.js'; + +const cwd = resolve('fixtures/plugins/graphql-codegen'); +const manifest = getManifest(cwd); + +test('Find dependencies in graphql-codegen configuration (json)', async () => { + const configFilePath = join(cwd, 'package.json'); + const dependencies = await graphqlCodegen.findDependencies(configFilePath, { manifest, config }); + assert.deepEqual(dependencies, ['@graphql-codegen/client-preset']); +}); + +test('Find dependencies in graphql-codegen configuration (codegen.ts)', async () => { + const configFilePath = join(cwd, 'codegen.ts'); + const dependencies = await graphqlCodegen.findDependencies(configFilePath, { manifest, config }); + assert.deepEqual(dependencies, [ + '@graphql-codegen/near-operation-file-preset', + '@graphql-codegen/schema-ast', + '@graphql-codegen/introspection', + '@graphql-codegen/typescript', + '@graphql-codegen/typescript-operations', + '@graphql-codegen/typescript-urql', + '@graphql-codegen/typescript-operations', + '@graphql-codegen/typescript-msw', + ]); +}); + +test('Find dependencies in graphql-codegen configuration (codegen.ts function)', async () => { + const { issues, counters } = await main({ + ...baseArguments, + cwd, + }); + + assert(issues.unlisted['codegen.ts']['@graphql-codegen/near-operation-file-preset']); + assert(issues.unlisted['codegen.ts']['@graphql-codegen/schema-ast']); + assert(issues.unlisted['codegen.ts']['@graphql-codegen/introspection']); + assert(issues.unlisted['codegen.ts']['@graphql-codegen/typescript']); + assert(issues.unlisted['codegen.ts']['@graphql-codegen/typescript-operations']); + assert(issues.unlisted['codegen.ts']['@graphql-codegen/typescript-urql']); + assert(issues.unlisted['codegen.ts']['@graphql-codegen/typescript-msw']); + assert(issues.unlisted['package.json']['@graphql-codegen/client-preset']); + + assert.deepEqual(counters, { + ...baseCounters, + unlisted: 8, + devDependencies: 1, + processed: 1, + total: 1, + }); +});