ESLint plugin providing linting rules for Relay GraphQL applications with syntax validation, naming conventions, and best practices enforcement.
npx @tessl/cli install tessl/npm-eslint-plugin-relay@2.0.0ESLint Plugin Relay is a comprehensive ESLint plugin designed specifically for Relay GraphQL applications. It provides 8 specialized linting rules that catch common problems in Relay code early in development, validating GraphQL syntax, enforcing naming conventions, detecting unused fields, and ensuring proper fragment colocation patterns.
npm install --save-dev eslint-plugin-relayThe plugin is imported and configured through ESLint configuration files:
// .eslintrc.js
module.exports = {
plugins: ['relay'],
rules: {
'relay/graphql-syntax': 'error',
'relay/graphql-naming': 'error',
// ... other rules
}
};For configuration presets:
// .eslintrc.js
module.exports = {
extends: ['plugin:relay/recommended']
};// .eslintrc.js
module.exports = {
plugins: ['relay'],
rules: {
'relay/graphql-syntax': 'error',
'relay/graphql-naming': 'error',
'relay/must-colocate-fragment-spreads': 'warn',
'relay/no-future-added-value': 'warn',
'relay/unused-fields': 'warn',
'relay/function-required-argument': 'warn',
'relay/hook-required-argument': 'warn'
}
};// Basic recommended rules
{
"extends": ["plugin:relay/recommended"]
}
// TypeScript recommended rules
{
"extends": ["plugin:relay/ts-recommended"]
}
// Strict configuration (all errors)
{
"extends": ["plugin:relay/strict"]
}
// TypeScript strict configuration
{
"extends": ["plugin:relay/ts-strict"]
}ESLint Plugin Relay is structured around:
Validates syntax of graphql tagged template literals to catch parsing errors early.
// Rule: 'relay/graphql-syntax'
// Validates that graphql`` tagged templates contain valid GraphQL syntax
// Reports errors for:
// - Invalid GraphQL syntax
// - Template substitutions (${...} not allowed)
// - Multiple definitions in single template
// - Operations without namesUsage Example:
// Valid
const query = graphql`
query MyComponentQuery {
user(id: "123") {
name
}
}
`;
// Invalid - syntax error will be caught
const badQuery = graphql`
query {
user(id: "123" { // Missing closing parenthesis
name
}
}
`;Enforces Relay's naming conventions for GraphQL fragments and queries.
// Rule: 'relay/graphql-naming'
// Validates naming patterns:
// - Queries must start with component name
// - Fragment names must follow <ComponentName>_<propName> pattern
// Provides auto-fixing capabilitiesUsage Example:
// Valid naming
const MyComponentQuery = graphql`
query MyComponentQuery {
viewer { name }
}
`;
const MyComponentFragment = graphql`
fragment MyComponent_user on User {
name
email
}
`;
// Invalid - will be flagged
const BadQuery = graphql`
query SomeOtherName { // Should start with 'MyComponent'
viewer { name }
}
`;Validates and manages TypeScript type imports for generated Relay types.
// Rule: 'relay/generated-typescript-types'
// Schema: {
// type: 'object',
// properties: {
// fix: { type: 'boolean' }, // Auto-fix missing type imports
// haste: { type: 'boolean' } // Use Haste module resolution
// },
// additionalProperties: false
// }
// Manages TypeScript imports for Relay-generated types
// Can auto-fix missing type imports when fix: true
// Supports Haste module resolution when haste: trueConfiguration Examples:
// Basic usage (no auto-fixing)
{
"rules": {
"relay/generated-typescript-types": "warn"
}
}
// With auto-fixing enabled
{
"rules": {
"relay/generated-typescript-types": ["warn", { "fix": true }]
}
}
// With Haste module resolution
{
"rules": {
"relay/generated-typescript-types": ["warn", { "haste": true, "fix": true }]
}
}Prevents explicit handling of Relay's "%future added value" enum placeholder.
// Rule: 'relay/no-future-added-value'
// Prevents using '%future added value' literal
// Catches attempts to explicitly handle future enum variantsUsage Example:
// Invalid - will be flagged
if (status === '%future added value') {
// This should not be explicitly handled
}
// Valid - let default case handle future values
switch (status) {
case 'ACTIVE':
return 'User is active';
case 'INACTIVE':
return 'User is inactive';
default:
return 'Unknown status';
}Ensures all GraphQL fields queried are actually used in the component.
// Rule: 'relay/unused-fields'
// Detects GraphQL fields that are queried but not used
// Supports suppression: # eslint-disable-next-line relay/unused-fields
// Ignores pageInfo fields and __typename by defaultUsage Example:
// Invalid - 'email' field is queried but not used
const fragment = graphql`
fragment MyComponent_user on User {
name
email // This will be flagged as unused
}
`;
function MyComponent({ user }) {
return <div>{user.name}</div>; // Only 'name' is used
}Ensures fragment spreads are colocated with components that use them.
// Rule: 'relay/must-colocate-fragment-spreads'
// Validates that fragment spreads are imported from defining modules
// Prevents anti-pattern of fetching data for child components
// Supports suppression: # eslint-disable-next-line relay/must-colocate-fragment-spreadsUsage Example:
// Valid - fragment spread colocated with import
import ChildComponent from './ChildComponent';
const fragment = graphql`
fragment ParentComponent_data on Data {
...ChildComponent_data // Valid if ChildComponent exports this fragment
}
`;
// Invalid - using fragment without importing defining module
const badFragment = graphql`
fragment ParentComponent_data on Data {
...SomeOtherComponent_data // No import for SomeOtherComponent
}
`;Ensures readInlineData function always receives explicit arguments.
// Rule: 'relay/function-required-argument'
// Validates that readInlineData() always has 2 arguments
// Prevents runtime errors from missing fragment referencesUsage Example:
// Valid
const data = readInlineData(fragment, props.data);
// Invalid - missing second argument
const data = readInlineData(fragment); // Will be flaggedEnsures Relay hooks always receive explicit arguments.
// Rule: 'relay/hook-required-argument'
// Validates that Relay hooks always have required arguments:
// - useFragment(fragment, fragmentRef)
// - usePaginationFragment(fragment, fragmentRef)
// - useBlockingPaginationFragment(fragment, fragmentRef)
// - useLegacyPaginationFragment(fragment, fragmentRef)
// - useRefetchableFragment(fragment, fragmentRef)Usage Example:
// Valid
const data = useFragment(fragment, props.user);
const { data, loadNext } = usePaginationFragment(fragment, props.connection);
// Invalid - missing fragment reference arguments
const data = useFragment(fragment); // Will be flagged
const { data } = usePaginationFragment(fragment); // Will be flaggedStandard rule set for general Relay development.
const recommended = {
rules: {
'relay/graphql-syntax': 'error',
'relay/graphql-naming': 'error',
'relay/no-future-added-value': 'warn',
'relay/unused-fields': 'warn',
'relay/must-colocate-fragment-spreads': 'warn',
'relay/function-required-argument': 'warn',
'relay/hook-required-argument': 'warn'
}
};Includes TypeScript-specific rules for Relay projects.
const tsRecommended = {
rules: {
'relay/graphql-syntax': 'error',
'relay/graphql-naming': 'error',
'relay/generated-typescript-types': 'warn',
'relay/no-future-added-value': 'warn',
'relay/unused-fields': 'warn',
'relay/must-colocate-fragment-spreads': 'warn',
'relay/function-required-argument': 'warn',
'relay/hook-required-argument': 'warn'
}
};All rules set to error level for maximum enforcement.
const strict = {
rules: {
'relay/graphql-syntax': 'error',
'relay/graphql-naming': 'error',
'relay/no-future-added-value': 'error',
'relay/unused-fields': 'error',
'relay/must-colocate-fragment-spreads': 'error',
'relay/function-required-argument': 'error',
'relay/hook-required-argument': 'error'
}
};TypeScript strict rules with all rules as errors.
const tsStrict = {
rules: {
'relay/graphql-syntax': 'error',
'relay/graphql-naming': 'error',
'relay/generated-typescript-types': 'error',
'relay/no-future-added-value': 'error',
'relay/unused-fields': 'error',
'relay/must-colocate-fragment-spreads': 'error',
'relay/function-required-argument': 'error',
'relay/hook-required-argument': 'error'
}
};// Main plugin export structure
interface ESLintPluginRelay {
rules: {
'graphql-syntax': ESLintRule;
'graphql-naming': ESLintRule;
'generated-typescript-types': ESLintRule;
'no-future-added-value': ESLintRule;
'unused-fields': ESLintRule;
'must-colocate-fragment-spreads': ESLintRule;
'function-required-argument': ESLintRule;
'hook-required-argument': ESLintRule;
};
configs: {
recommended: ESLintConfig;
'ts-recommended': ESLintConfig;
strict: ESLintConfig;
'ts-strict': ESLintConfig;
};
}
interface ESLintRule {
meta: {
docs?: { description: string };
fixable?: 'code';
schema?: any[];
};
create: (context: ESLintContext) => ESLintVisitor;
}
interface ESLintConfig {
rules: Record<string, 'error' | 'warn' | 'off'>;
}Some rules support inline suppression within GraphQL tagged templates:
// Supported for unused-fields and must-colocate-fragment-spreads
const fragment = graphql`
fragment MyComponent_data on Data {
# eslint-disable-next-line relay/unused-fields
unusedField
# eslint-disable-next-line relay/must-colocate-fragment-spreads
...NonColocatedFragment_data
}
`;Note: Only eslint-disable-next-line format works within GraphQL templates. eslint-disable-line is not supported due to GraphQL AST limitations.