diff --git a/.changeset/hot-worms-unite.md b/.changeset/hot-worms-unite.md new file mode 100644 index 00000000000..e76c291609d --- /dev/null +++ b/.changeset/hot-worms-unite.md @@ -0,0 +1,11 @@ +--- +'@graphql-eslint/eslint-plugin': minor +--- + +Improve `parseForESLint` API and allow to pass context schema as inline string. + +You can now use it this way: + +```ts +parseForESLint(code, { schemaSdl: 'type Query { foo: String }', filePath: 'test.graphql' }); +``` diff --git a/package.json b/package.json index 7a356b56a9e..3169799012d 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "packageManager": "pnpm@10.6.5", "engines": { "node": ">=16", - "pnpm": ">=9.0.6" + "pnpm": ">=10.6" }, "scripts": { "build": "turbo run build --filter=!website && attw --pack packages/plugin/dist", diff --git a/packages/plugin/__tests__/__snapshots__/parser.spec.md b/packages/plugin/__tests__/__snapshots__/parser.spec.md index 2bb6f0fd4cf..f382d1c9e87 100644 --- a/packages/plugin/__tests__/__snapshots__/parser.spec.md +++ b/packages/plugin/__tests__/__snapshots__/parser.spec.md @@ -206,3 +206,355 @@ exports[`Parser > parseForESLint() should return ast and tokens 1`] = ` type: Program, } `; + +exports[`Parser > should allow to pass inline schema string as input 1`] = ` +{ + body: [ + { + definitions: [ + { + description: undefined, + directives: [], + fields: [ + { + arguments: [], + description: undefined, + directives: [], + gqlType: { + kind: NamedType, + leadingComments: [], + loc: { + end: { + column: 19, + line: 3, + }, + source: + type Query { + foo: String + } + , + start: { + column: 13, + line: 3, + }, + }, + name: { + kind: Name, + leadingComments: [], + loc: { + end: { + column: 19, + line: 3, + }, + source: + type Query { + foo: String + } + , + start: { + column: 13, + line: 3, + }, + }, + range: [ + 33, + 39, + ], + rawNode: [Function], + type: Name, + typeInfo: [Function], + value: String, + }, + range: [ + 33, + 39, + ], + rawNode: [Function], + type: NamedType, + typeInfo: [Function], + }, + kind: FieldDefinition, + leadingComments: [], + loc: { + end: { + column: 13, + line: 3, + }, + source: + type Query { + foo: String + } + , + start: { + column: 8, + line: 3, + }, + }, + name: { + kind: Name, + leadingComments: [], + loc: { + end: { + column: 11, + line: 3, + }, + source: + type Query { + foo: String + } + , + start: { + column: 8, + line: 3, + }, + }, + range: [ + 28, + 31, + ], + rawNode: [Function], + type: Name, + typeInfo: [Function], + value: foo, + }, + range: [ + 28, + 39, + ], + rawNode: [Function], + type: FieldDefinition, + typeInfo: [Function], + }, + ], + interfaces: [], + kind: ObjectTypeDefinition, + leadingComments: [], + loc: { + end: { + column: 46, + line: 4, + }, + source: + type Query { + foo: String + } + , + start: { + column: 6, + line: 2, + }, + }, + name: { + kind: Name, + leadingComments: [], + loc: { + end: { + column: 16, + line: 2, + }, + source: + type Query { + foo: String + } + , + start: { + column: 11, + line: 2, + }, + }, + range: [ + 12, + 17, + ], + rawNode: [Function], + type: Name, + typeInfo: [Function], + value: Query, + }, + range: [ + 7, + 47, + ], + rawNode: [Function], + type: ObjectTypeDefinition, + typeInfo: [Function], + }, + ], + kind: Document, + leadingComments: [], + loc: { + end: { + column: 4, + line: 5, + }, + source: + type Query { + foo: String + } + , + start: { + column: 0, + line: 1, + }, + }, + range: [ + 0, + 52, + ], + rawNode: [Function], + type: Document, + typeInfo: [Function], + }, + ], + comments: [], + loc: { + end: { + column: 4, + line: 5, + }, + source: + type Query { + foo: String + } + , + start: { + column: 0, + line: 1, + }, + }, + range: [ + 0, + 52, + ], + sourceType: script, + tokens: [ + { + loc: { + end: { + column: 10, + line: 2, + }, + start: { + column: 6, + line: 2, + }, + }, + range: [ + 7, + 11, + ], + type: Name, + value: type, + }, + { + loc: { + end: { + column: 16, + line: 2, + }, + start: { + column: 11, + line: 2, + }, + }, + range: [ + 12, + 17, + ], + type: Name, + value: Query, + }, + { + loc: { + end: { + column: 18, + line: 2, + }, + start: { + column: 17, + line: 2, + }, + }, + range: [ + 18, + 19, + ], + type: {, + value: undefined, + }, + { + loc: { + end: { + column: 11, + line: 3, + }, + start: { + column: 8, + line: 3, + }, + }, + range: [ + 28, + 31, + ], + type: Name, + value: foo, + }, + { + loc: { + end: { + column: 12, + line: 3, + }, + start: { + column: 11, + line: 3, + }, + }, + range: [ + 31, + 32, + ], + type: :, + value: undefined, + }, + { + loc: { + end: { + column: 19, + line: 3, + }, + start: { + column: 13, + line: 3, + }, + }, + range: [ + 33, + 39, + ], + type: Name, + value: String, + }, + { + loc: { + end: { + column: 7, + line: 4, + }, + start: { + column: 6, + line: 4, + }, + }, + range: [ + 46, + 47, + ], + type: }, + value: undefined, + }, + ], + type: Program, +} +`; diff --git a/packages/plugin/__tests__/parser.spec.ts b/packages/plugin/__tests__/parser.spec.ts index 9487e096e7d..b9df7846b49 100644 --- a/packages/plugin/__tests__/parser.spec.ts +++ b/packages/plugin/__tests__/parser.spec.ts @@ -1,6 +1,18 @@ import { parseForESLint } from '../src/parser.js'; describe('Parser', () => { + it('should allow to pass inline schema string as input', () => { + const code = /* GraphQL */ ` + type Query { + foo: String + } + `; + + const result = parseForESLint(code, { schemaSdl: code, filePath: 'test.graphql' }); + expect(result.ast).toMatchSnapshot(); + expect(result.ast.tokens).toBeTruthy(); + }); + it('parseForESLint() should return ast and tokens', () => { const code = /* GraphQL */ ` """ diff --git a/packages/plugin/__tests__/test-utils.ts b/packages/plugin/__tests__/test-utils.ts index c79d4b87589..a3bf9e709d2 100644 --- a/packages/plugin/__tests__/test-utils.ts +++ b/packages/plugin/__tests__/test-utils.ts @@ -1,7 +1,6 @@ import { Linter } from 'eslint'; -import graphqlPlugin from '@graphql-eslint/eslint-plugin'; +import graphqlPlugin, { type ParserConfigGraphQLConfig } from '@graphql-eslint/eslint-plugin'; import { RuleTester } from '@theguild/eslint-rule-tester'; -import { ParserOptions } from '../src/index.js'; export const DEFAULT_CONFIG: Linter.Config = { languageOptions: { @@ -10,7 +9,7 @@ export const DEFAULT_CONFIG: Linter.Config = { }; export type ParserOptionsForTests = { - graphQLConfig: Partial; + graphQLConfig: Partial; }; export const ruleTester = new RuleTester(DEFAULT_CONFIG); diff --git a/packages/plugin/src/graphql-config.ts b/packages/plugin/src/graphql-config.ts index e95ce7b00b6..df96c3f6ea9 100644 --- a/packages/plugin/src/graphql-config.ts +++ b/packages/plugin/src/graphql-config.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import debugFactory from 'debug'; import { GraphQLConfig, GraphQLExtensionDeclaration, loadConfigSync } from 'graphql-config'; import { CodeFileLoader } from '@graphql-tools/code-file-loader'; -import { ParserOptions } from './types.js'; +import { ParserConfigGraphQLConfig } from './types.js'; const debug = debugFactory('graphql-eslint:graphql-config'); let graphQLConfig: GraphQLConfig; @@ -31,7 +31,7 @@ export function loadOnDiskGraphQLConfig(filePath: string): GraphQLConfig { export function loadGraphQLConfig({ graphQLConfig: config, filePath, -}: ParserOptions): GraphQLConfig { +}: ParserConfigGraphQLConfig): GraphQLConfig { // We don't want cache config on test environment // Otherwise schema and documents will be same for all tests if (process.env.NODE_ENV !== 'test' && graphQLConfig) { diff --git a/packages/plugin/src/parser.ts b/packages/plugin/src/parser.ts index 63101189ab7..a0d358f31cc 100644 --- a/packages/plugin/src/parser.ts +++ b/packages/plugin/src/parser.ts @@ -43,32 +43,38 @@ export function parseForESLint(code: string, options: ParserOptions): GraphQLESL // documents or even graphql-config instance const { document } = parseGraphQLSDL(filePath, code, { noLocation: false }); let project: GraphQLProjectConfig; - let schema: Schema, documents: Source[]; - if (typeof window === 'undefined') { - const gqlConfig = loadGraphQLConfig(options); - project = gqlConfig.getProjectForFile(getFirstExistingPath(filePath)); - documents = getDocuments(project); - } else { - documents = [ - parseGraphQLSDL( - 'operation.graphql', - (options.graphQLConfig as IGraphQLProject).documents as string, - { noLocation: true }, - ), - ]; - } + let schema: Schema, + documents: Source[] = []; - try { + if ('schemaSdl' in options) { + schema = buildSchema(options.schemaSdl); + } else { if (typeof window === 'undefined') { - schema = getSchema(project!); + const gqlConfig = loadGraphQLConfig(options); + project = gqlConfig.getProjectForFile(getFirstExistingPath(filePath)); + documents = getDocuments(project); } else { - schema = buildSchema((options.graphQLConfig as IGraphQLProject).schema as string); + documents = [ + parseGraphQLSDL( + 'operation.graphql', + (options.graphQLConfig as IGraphQLProject).documents as string, + { noLocation: true }, + ), + ]; } - } catch (error) { - if (error instanceof Error) { - error.message = `Error while loading schema: ${error.message}`; + + try { + if (typeof window === 'undefined') { + schema = getSchema(project!); + } else { + schema = buildSchema((options.graphQLConfig as IGraphQLProject).schema as string); + } + } catch (error) { + if (error instanceof Error) { + error.message = `Error while loading schema: ${error.message}`; + } + throw error; } - throw error; } const rootTree = convertToESTree(document, schema); diff --git a/packages/plugin/src/types.ts b/packages/plugin/src/types.ts index 33ae8668c5f..b43adca4d0c 100644 --- a/packages/plugin/src/types.ts +++ b/packages/plugin/src/types.ts @@ -10,10 +10,17 @@ export type Schema = GraphQLSchema | null; export type Pointer = string | string[]; export type { GraphQLESTreeNode } from './estree-converter/types.js'; -export interface ParserOptions { +export type ParserConfigGraphQLConfig = { graphQLConfig?: IGraphQLConfig; filePath: string; -} +}; + +export type ParserConfigProgrammatic = { + schemaSdl: string; + filePath: string; +}; + +export type ParserOptions = ParserConfigGraphQLConfig | ParserConfigProgrammatic; export type ParserServices = { schema: Schema; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index abaf66b000a..bda662543bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1664,8 +1664,8 @@ packages: resolution: {integrity: sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.3.0': - resolution: {integrity: sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==} + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/js@9.22.0': @@ -9575,7 +9575,7 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.0': + '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 debug: 4.4.0 @@ -12757,7 +12757,7 @@ snapshots: '@eslint/config-array': 0.19.2 '@eslint/config-helpers': 0.1.0 '@eslint/core': 0.12.0 - '@eslint/eslintrc': 3.3.0 + '@eslint/eslintrc': 3.3.1 '@eslint/js': 9.22.0 '@eslint/plugin-kit': 0.2.7 '@humanfs/node': 0.16.6 diff --git a/website/content/rules/naming-convention.mdx b/website/content/rules/naming-convention.mdx index e144adfea60..c2d0dba90bb 100644 --- a/website/content/rules/naming-convention.mdx +++ b/website/content/rules/naming-convention.mdx @@ -35,7 +35,7 @@ type user { ### Incorrect ```graphql -# eslint @graphql-eslint/naming-convention: ['error', { FragmentDefinition: { style: 'PascalCase', forbiddenPatterns: ['/fragment$/i'] } }] +# eslint @graphql-eslint/naming-convention: ['error', { FragmentDefinition: { style: 'PascalCase', forbiddenPatterns: ['/(^fragment)|(fragment$)/i'] } }] fragment UserFragment on User { # ... @@ -65,7 +65,7 @@ type User { ### Correct ```graphql -# eslint @graphql-eslint/naming-convention: ['error', { FragmentDefinition: { style: 'PascalCase', forbiddenPatterns: ['/fragment$/i'] } }] +# eslint @graphql-eslint/naming-convention: ['error', { FragmentDefinition: { style: 'PascalCase', forbiddenPatterns: ['/(^fragment)|(fragment$)/i'] } }] fragment UserFields on User { # ... diff --git a/website/next-env.d.ts b/website/next-env.d.ts index 40c3d68096c..1b3be0840f3 100644 --- a/website/next-env.d.ts +++ b/website/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.