@graphql-eslint/eslint-plugin
Version:
GraphQL plugin for ESLint
233 lines (232 loc) • 7.84 kB
JavaScript
import { basename, extname } from 'path';
import { existsSync } from 'fs';
import { Kind } from 'graphql';
import { convertCase, REPORT_ON_FIRST_CHARACTER } from '../utils.js';
const MATCH_EXTENSION = 'MATCH_EXTENSION';
const MATCH_STYLE = 'MATCH_STYLE';
const CASE_STYLES = [
'camelCase',
'PascalCase',
'snake_case',
'UPPER_CASE',
'kebab-case',
'matchDocumentStyle',
];
const schemaOption = {
oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asObject' }],
};
const schema = {
definitions: {
asString: {
enum: CASE_STYLES,
description: `One of: ${CASE_STYLES.map(t => `\`${t}\``).join(', ')}`,
},
asObject: {
type: 'object',
additionalProperties: false,
minProperties: 1,
properties: {
style: { enum: CASE_STYLES },
suffix: { type: 'string' },
prefix: { type: 'string' },
},
},
},
type: 'array',
minItems: 1,
maxItems: 1,
items: {
type: 'object',
additionalProperties: false,
minProperties: 1,
properties: {
fileExtension: { enum: ['.gql', '.graphql'] },
query: schemaOption,
mutation: schemaOption,
subscription: schemaOption,
fragment: schemaOption,
},
},
};
export const rule = {
meta: {
type: 'suggestion',
docs: {
category: 'Operations',
description: 'This rule allows you to enforce that the file name should match the operation name.',
url: 'https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/match-document-filename.md',
examples: [
{
title: 'Correct',
usage: [{ fileExtension: '.gql' }],
code: /* GraphQL */ `
# user.gql
type User {
id: ID!
}
`,
},
{
title: 'Correct',
usage: [{ query: 'snake_case' }],
code: /* GraphQL */ `
# user_by_id.gql
query UserById {
userById(id: 5) {
id
name
fullName
}
}
`,
},
{
title: 'Correct',
usage: [{ fragment: { style: 'kebab-case', suffix: '.fragment' } }],
code: /* GraphQL */ `
# user-fields.fragment.gql
fragment user_fields on User {
id
email
}
`,
},
{
title: 'Correct',
usage: [{ mutation: { style: 'PascalCase', suffix: 'Mutation' } }],
code: /* GraphQL */ `
# DeleteUserMutation.gql
mutation DELETE_USER {
deleteUser(id: 5)
}
`,
},
{
title: 'Incorrect',
usage: [{ fileExtension: '.graphql' }],
code: /* GraphQL */ `
# post.gql
type Post {
id: ID!
}
`,
},
{
title: 'Incorrect',
usage: [{ query: 'PascalCase' }],
code: /* GraphQL */ `
# user-by-id.gql
query UserById {
userById(id: 5) {
id
name
fullName
}
}
`,
},
{
title: 'Correct',
usage: [{ fragment: { style: 'kebab-case', prefix: 'mutation.' } }],
code: /* GraphQL */ `
# mutation.add-alert.graphql
mutation addAlert {
foo
}
`,
},
{
title: 'Correct',
usage: [{ fragment: { prefix: 'query.' } }],
code: /* GraphQL */ `
# query.me.graphql
query me {
foo
}
`,
},
],
configOptions: [
{
query: 'kebab-case',
mutation: 'kebab-case',
subscription: 'kebab-case',
fragment: 'kebab-case',
},
],
},
messages: {
[MATCH_EXTENSION]: 'File extension "{{ fileExtension }}" don\'t match extension "{{ expectedFileExtension }}"',
[MATCH_STYLE]: 'Unexpected filename "{{ filename }}". Rename it to "{{ expectedFilename }}"',
},
schema,
},
create(context) {
const options = context.options[0] || {
fileExtension: null,
};
const filePath = context.getFilename();
const isVirtualFile = !existsSync(filePath);
if (process.env.NODE_ENV !== 'test' && isVirtualFile) {
// Skip validation for code files
return {};
}
const fileExtension = extname(filePath);
const filename = basename(filePath, fileExtension);
return {
Document(documentNode) {
var _a;
if (options.fileExtension && options.fileExtension !== fileExtension) {
context.report({
loc: REPORT_ON_FIRST_CHARACTER,
messageId: MATCH_EXTENSION,
data: {
fileExtension,
expectedFileExtension: options.fileExtension,
},
});
}
const firstOperation = documentNode.definitions.find(n => n.kind === Kind.OPERATION_DEFINITION);
const firstFragment = documentNode.definitions.find(n => n.kind === Kind.FRAGMENT_DEFINITION);
const node = firstOperation || firstFragment;
if (!node) {
return;
}
const docName = (_a = node.name) === null || _a === void 0 ? void 0 : _a.value;
if (!docName) {
return;
}
const docType = 'operation' in node ? node.operation : 'fragment';
let option = options[docType];
if (!option) {
// Config not provided
return;
}
if (typeof option === 'string') {
option = { style: option };
}
const expectedExtension = options.fileExtension || fileExtension;
let expectedFilename = option.prefix || '';
if (option.style) {
expectedFilename +=
option.style === 'matchDocumentStyle' ? docName : convertCase(option.style, docName);
}
else {
expectedFilename += filename;
}
expectedFilename += (option.suffix || '') + expectedExtension;
const filenameWithExtension = filename + expectedExtension;
if (expectedFilename !== filenameWithExtension) {
context.report({
loc: REPORT_ON_FIRST_CHARACTER,
messageId: MATCH_STYLE,
data: {
expectedFilename,
filename: filenameWithExtension,
},
});
}
},
};
},
};