@graphql-eslint/eslint-plugin
Version:
GraphQL plugin for ESLint
263 lines (256 loc) • 7.5 kB
JavaScript
Object.defineProperty(exports, "__esModule", {value: true}); function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }var _nodepath = require('node:path');
var _graphql = require('graphql');
var _utilsjs = require('../../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 caseSchema = {
enum: CASE_STYLES,
description: `One of: ${CASE_STYLES.map((t) => `\`${t}\``).join(", ")}`
};
const schema = {
definitions: {
asString: caseSchema,
asObject: {
type: "object",
additionalProperties: false,
minProperties: 1,
properties: {
style: caseSchema,
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
}
}
};
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://the-guild.dev/graphql/eslint/rules/match-document-filename",
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.filename;
const isVirtualFile = _utilsjs.VIRTUAL_DOCUMENT_REGEX.test(filePath);
if (process.env.NODE_ENV !== "test" && isVirtualFile) {
return {};
}
const fileExtension = _nodepath.extname.call(void 0, filePath);
const filename = _nodepath.basename.call(void 0, filePath, fileExtension);
return {
Document(documentNode) {
if (options.fileExtension && options.fileExtension !== fileExtension) {
context.report({
loc: _utilsjs.REPORT_ON_FIRST_CHARACTER,
messageId: MATCH_EXTENSION,
data: {
fileExtension,
expectedFileExtension: options.fileExtension
}
});
}
const firstOperation = documentNode.definitions.find(
(n) => n.kind === _graphql.Kind.OPERATION_DEFINITION
);
const firstFragment = documentNode.definitions.find(
(n) => n.kind === _graphql.Kind.FRAGMENT_DEFINITION
);
const node = firstOperation || firstFragment;
if (!node) {
return;
}
const docName = _optionalChain([node, 'access', _ => _.name, 'optionalAccess', _2 => _2.value]);
if (!docName) {
return;
}
const docType = "operation" in node ? node.operation : "fragment";
let option = options[docType];
if (!option) {
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 : _utilsjs.convertCase.call(void 0, option.style, docName);
} else {
expectedFilename += filename;
}
expectedFilename += (option.suffix || "") + expectedExtension;
const filenameWithExtension = filename + expectedExtension;
if (expectedFilename !== filenameWithExtension) {
context.report({
loc: _utilsjs.REPORT_ON_FIRST_CHARACTER,
messageId: MATCH_STYLE,
data: {
expectedFilename,
filename: filenameWithExtension
}
});
}
}
};
}
};
exports.rule = rule;
;