eslint-plugin-relay
Advanced tools
Comparing version
@@ -13,5 +13,4 @@ /** | ||
'graphql-syntax': require('./src/rule-graphql-syntax'), | ||
'compat-uses-vars': require('./src/rule-compat-uses-vars'), | ||
'graphql-naming': require('./src/rule-graphql-naming'), | ||
'generated-flow-types': require('./src/rule-generated-flow-types'), | ||
'generated-typescript-types': require('./src/rule-generated-typescript-types'), | ||
'no-future-added-value': require('./src/rule-no-future-added-value'), | ||
@@ -27,5 +26,3 @@ 'unused-fields': require('./src/rule-unused-fields'), | ||
'relay/graphql-syntax': 'error', | ||
'relay/compat-uses-vars': 'warn', | ||
'relay/graphql-naming': 'error', | ||
'relay/generated-flow-types': 'warn', | ||
'relay/no-future-added-value': 'warn', | ||
@@ -38,8 +35,18 @@ 'relay/unused-fields': 'warn', | ||
}, | ||
'ts-recommended': { | ||
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' | ||
} | ||
}, | ||
strict: { | ||
rules: { | ||
'relay/graphql-syntax': 'error', | ||
'relay/compat-uses-vars': 'error', | ||
'relay/graphql-naming': 'error', | ||
'relay/generated-flow-types': 'error', | ||
'relay/no-future-added-value': 'error', | ||
@@ -51,4 +58,16 @@ 'relay/unused-fields': 'error', | ||
} | ||
}, | ||
'ts-strict': { | ||
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' | ||
} | ||
} | ||
} | ||
}; |
{ | ||
"name": "eslint-plugin-relay", | ||
"version": "1.8.3", | ||
"version": "2.0.0", | ||
"description": "ESLint plugin for Relay.", | ||
@@ -21,12 +21,18 @@ "main": "eslint-plugin-relay", | ||
"dependencies": { | ||
"graphql": "^14.0.0 || ^15.0.0" | ||
"graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" | ||
}, | ||
"devDependencies": { | ||
"babel-eslint": "^10.1.0", | ||
"eslint": "^7.8.0", | ||
"eslint-config-prettier": "^6.11.0", | ||
"eslint-plugin-prettier": "^3.1.4", | ||
"mocha": "^6.0.2", | ||
"prettier": "^2.0.5" | ||
} | ||
"@babel/core": "^7.26.10", | ||
"@babel/eslint-parser": "^7.27.0", | ||
"@babel/preset-flow": "^7.25.9", | ||
"@babel/preset-react": "^7.26.3", | ||
"@eslint/js": "^9.24.0", | ||
"@typescript-eslint/parser": "^8.29.1", | ||
"eslint": "^9.24.0", | ||
"globals": "^16.0.0", | ||
"mocha": "^9.1.3", | ||
"prettier": "^2.4.1", | ||
"typescript": "^5.8.3" | ||
}, | ||
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610" | ||
} |
@@ -21,5 +21,3 @@ # eslint-plugin-relay [](https://travis-ci.org/relayjs/eslint-plugin-relay) [](http://badge.fury.io/js/eslint-plugin-relay) | ||
'relay/graphql-syntax': 'error', | ||
'relay/compat-uses-vars': 'warn', | ||
'relay/graphql-naming': 'error', | ||
'relay/generated-flow-types': 'warn', | ||
'relay/must-colocate-fragment-spreads': 'warn', | ||
@@ -46,2 +44,14 @@ 'relay/no-future-added-value': 'warn', | ||
### Rule Descriptions | ||
Brief descriptions for each rule: | ||
- `relay/graphql-syntax`: Ensures each `graphql\`\`` tagged template literal contains syntactically valid GraphQL. This is also validated by the Relay Compiler, but the ESLint plugin can often provide faster feedback. | ||
- `relay/graphql-naming`: Ensures GraphQL fragments and queries follow Relay's naming conventions. This is also validated by the Relay Compiler, but the ESLint plugin can often provide faster feedback. | ||
- `relay/no-future-added-value`: Ensures code does not try to explicitly handle the `"%future added value"` enum variant which Relay inserts as a placeholder to ensure you handle the possibility that new enum variants may be added by the server after your application has been deployed. | ||
- `relay/unused-fields`: Ensures that every GraphQL field referenced is used within the module that includes it. This helps enable Relay's [optimal data fetching](https://relay.dev/blog/2023/10/24/how-relay-enables-optimal-data-fetching/) | ||
- `relay/function-required-argument`: Ensures that `readInlineData` is always passed an explicit argument even though that argument is allowed to be `undefined` at runtime. | ||
- `relay/hook-required-argument`: Ensures that Relay hooks are always passed an explicit argument even though that argument is allowed to be `undefined` at runtime. | ||
- `relay/must-colocate-fragment-spreads`: Ensures that when a fragment spread is added within a module, that module directly imports the module which defines that fragment. This prevents the anti-pattern when one component fetches a fragment that is not used by a direct child component. **Note**: This rule leans heavily on Meta's globally unique module names. It likely won't work well in other environments. | ||
### Suppressing rules within graphql tags | ||
@@ -57,6 +67,8 @@ | ||
```js | ||
graphql`fragment foo on Page { | ||
# eslint-disable-next-line relay/must-colocate-fragment-spreads | ||
...unused1 | ||
}` | ||
graphql` | ||
fragment foo on Page { | ||
# eslint-disable-next-line relay/must-colocate-fragment-spreads | ||
...unused1 | ||
} | ||
`; | ||
``` | ||
@@ -63,0 +75,0 @@ |
@@ -16,3 +16,2 @@ /** | ||
const isGraphQLTag = utils.isGraphQLTag; | ||
const isGraphQLDeprecatedTag = utils.isGraphQLDeprecatedTag; | ||
const shouldLint = utils.shouldLint; | ||
@@ -61,3 +60,3 @@ | ||
} | ||
const moduleName = getModuleName(context.getFilename()); | ||
const moduleName = getModuleName(context.filename ?? context.getFilename()); | ||
ast.definitions.forEach(def => { | ||
@@ -115,3 +114,5 @@ if (!def.name) { | ||
case 'OperationDefinition': { | ||
const moduleName = getModuleName(context.getFilename()); | ||
const moduleName = getModuleName( | ||
context.filename ?? context.getFilename() | ||
); | ||
const name = definition.name; | ||
@@ -154,6 +155,3 @@ if (!name) { | ||
) { | ||
if ( | ||
!isGraphQLTag(property.value.tag) && | ||
!isGraphQLDeprecatedTag(property.value.tag) | ||
) { | ||
if (!isGraphQLTag(property.value.tag)) { | ||
context.report({ | ||
@@ -160,0 +158,0 @@ node: property.value.tag, |
@@ -46,3 +46,5 @@ /** | ||
try { | ||
const filename = path.basename(context.getFilename()); | ||
const filename = path.basename( | ||
context.filename ?? context.getFilename() | ||
); | ||
const ast = parse(new Source(quasi.value.cooked, filename)); | ||
@@ -49,0 +51,0 @@ if (ast.definitions.length !== 1) { |
@@ -15,3 +15,3 @@ /** | ||
* the purpose of Relay. From the | ||
* [Relay docs](https://relay.dev/docs/en/next/introduction-to-relay) – "[Relay] | ||
* [Relay docs](https://relay.dev/docs/next) – "[Relay] | ||
* allows components to specify what data they need and the Relay framework | ||
@@ -117,77 +117,81 @@ * provides the data. This makes the data needs of inner components opaque and | ||
function rule(context) { | ||
const foundImportedModules = []; | ||
const graphqlLiterals = []; | ||
module.exports = { | ||
meta: { | ||
docs: {}, | ||
schema: [] | ||
}, | ||
create(context) { | ||
const foundImportedModules = []; | ||
const graphqlLiterals = []; | ||
return { | ||
'Program:exit'(_node) { | ||
const fragmentsInTheSameModule = []; | ||
graphqlLiterals.forEach(({graphQLAst}) => { | ||
const fragmentName = getGraphQLFragmentDefinitionName(graphQLAst); | ||
if (fragmentName) { | ||
fragmentsInTheSameModule.push(fragmentName); | ||
} | ||
}); | ||
graphqlLiterals.forEach(({node, graphQLAst}) => { | ||
const queriedFragments = getGraphQLFragmentSpreads(graphQLAst); | ||
for (const fragment in queriedFragments) { | ||
const matchedModuleName = foundImportedModules.find(name => | ||
fragment.startsWith(name) | ||
); | ||
if ( | ||
!matchedModuleName && | ||
!fragmentsInTheSameModule.includes(fragment) | ||
) { | ||
context.report({ | ||
node, | ||
loc: utils.getLoc(context, node, queriedFragments[fragment]), | ||
message: | ||
`This spreads the fragment \`${fragment}\` but ` + | ||
'this module does not use it directly. If a different module ' + | ||
'needs this information, that module should directly define a ' + | ||
'fragment querying for that data, colocated next to where the ' + | ||
'data is used.\n' | ||
}); | ||
return { | ||
'Program:exit'(_node) { | ||
const fragmentsInTheSameModule = []; | ||
graphqlLiterals.forEach(({graphQLAst}) => { | ||
const fragmentName = getGraphQLFragmentDefinitionName(graphQLAst); | ||
if (fragmentName) { | ||
fragmentsInTheSameModule.push(fragmentName); | ||
} | ||
}); | ||
graphqlLiterals.forEach(({node, graphQLAst}) => { | ||
const queriedFragments = getGraphQLFragmentSpreads(graphQLAst); | ||
for (const fragment in queriedFragments) { | ||
const matchedModuleName = foundImportedModules.find(name => | ||
fragment.startsWith(name) | ||
); | ||
if ( | ||
!matchedModuleName && | ||
!fragmentsInTheSameModule.includes(fragment) | ||
) { | ||
context.report({ | ||
node, | ||
loc: utils.getLoc(context, node, queriedFragments[fragment]), | ||
message: | ||
`This spreads the fragment \`${fragment}\` but ` + | ||
'this module does not use it directly. If a different module ' + | ||
'needs this information, that module should directly define a ' + | ||
'fragment querying for that data, colocated next to where the ' + | ||
'data is used.\n' | ||
}); | ||
} | ||
} | ||
}); | ||
}, | ||
ImportDeclaration(node) { | ||
if (node.importKind === 'value') { | ||
foundImportedModules.push(utils.getModuleName(node.source.value)); | ||
} | ||
}); | ||
}, | ||
}, | ||
ImportDeclaration(node) { | ||
if (node.importKind === 'value') { | ||
foundImportedModules.push(utils.getModuleName(node.source.value)); | ||
} | ||
}, | ||
ImportExpression(node) { | ||
if (node.source.type === 'Literal') { | ||
// Allow dynamic imports like import(`test/${fileName}`); and (path) => import(path); | ||
// These would have node.source.value undefined | ||
foundImportedModules.push(utils.getModuleName(node.source.value)); | ||
} | ||
}, | ||
ImportExpression(node) { | ||
if (node.source.type === 'Literal') { | ||
// Allow dynamic imports like import(`test/${fileName}`); and (path) => import(path); | ||
// These would have node.source.value undefined | ||
foundImportedModules.push(utils.getModuleName(node.source.value)); | ||
} | ||
}, | ||
CallExpression(node) { | ||
if (node.callee.name !== 'require') { | ||
return; | ||
} | ||
const [source] = node.arguments; | ||
if (source && source.type === 'Literal') { | ||
foundImportedModules.push(utils.getModuleName(source.value)); | ||
} | ||
}, | ||
TaggedTemplateExpression(node) { | ||
if (utils.isGraphQLTemplate(node)) { | ||
const graphQLAst = utils.getGraphQLAST(node); | ||
if (!graphQLAst) { | ||
// ignore nodes with syntax errors, they're handled by rule-graphql-syntax | ||
CallExpression(node) { | ||
if (node.callee.name !== 'require') { | ||
return; | ||
} | ||
graphqlLiterals.push({node, graphQLAst}); | ||
const [source] = node.arguments; | ||
if (source && source.type === 'Literal') { | ||
foundImportedModules.push(utils.getModuleName(source.value)); | ||
} | ||
}, | ||
TaggedTemplateExpression(node) { | ||
if (utils.isGraphQLTemplate(node)) { | ||
const graphQLAst = utils.getGraphQLAST(node); | ||
if (!graphQLAst) { | ||
// ignore nodes with syntax errors, they're handled by rule-graphql-syntax | ||
return; | ||
} | ||
graphqlLiterals.push({node, graphQLAst}); | ||
} | ||
} | ||
} | ||
}; | ||
} | ||
module.exports = rule; | ||
}; | ||
} | ||
}; |
@@ -10,17 +10,25 @@ /** | ||
module.exports = context => { | ||
function validateValue(node) { | ||
context.report( | ||
node, | ||
"Do not use `'%future added value'`. It represents any potential " + | ||
'value that the server might return in the future that the code ' + | ||
'should handle.' | ||
); | ||
module.exports = { | ||
meta: { | ||
docs: {}, | ||
schema: [] | ||
}, | ||
create: context => { | ||
function validateValue(node) { | ||
context.report({ | ||
node: node, | ||
message: | ||
"Do not use `'%future added value'`. It represents any potential " + | ||
'value that the server might return in the future that the code ' + | ||
'should handle.' | ||
}); | ||
} | ||
return { | ||
"Literal[value='%future added value']": validateValue, | ||
// StringLiteralTypeAnnotations that are not children of a default case | ||
":not(SwitchCase[test=null] StringLiteralTypeAnnotation)StringLiteralTypeAnnotation[value='%future added value']": | ||
validateValue | ||
}; | ||
} | ||
return { | ||
"Literal[value='%future added value']": validateValue, | ||
// StringLiteralTypeAnnotations that are not children of a default case | ||
":not(SwitchCase[test=null] StringLiteralTypeAnnotation)StringLiteralTypeAnnotation[value='%future added value']": validateValue | ||
}; | ||
}; |
@@ -95,115 +95,119 @@ /** | ||
function rule(context) { | ||
let currentMethod = []; | ||
let foundMemberAccesses = {}; | ||
let templateLiterals = []; | ||
module.exports = { | ||
meta: { | ||
docs: {}, | ||
schema: [] | ||
}, | ||
create(context) { | ||
let currentMethod = []; | ||
let foundMemberAccesses = {}; | ||
let templateLiterals = []; | ||
function visitGetByPathCall(node) { | ||
// The `getByPath` utility accesses nested fields in the form | ||
// `getByPath(thing, ['field', 'nestedField'])`. | ||
const pathArg = node.arguments[1]; | ||
if (!pathArg || pathArg.type !== 'ArrayExpression') { | ||
return; | ||
} | ||
pathArg.elements.forEach(element => { | ||
if (isStringNode(element)) { | ||
foundMemberAccesses[element.value] = true; | ||
function visitGetByPathCall(node) { | ||
// The `getByPath` utility accesses nested fields in the form | ||
// `getByPath(thing, ['field', 'nestedField'])`. | ||
const pathArg = node.arguments[1]; | ||
if (!pathArg || pathArg.type !== 'ArrayExpression') { | ||
return; | ||
} | ||
}); | ||
} | ||
function visitDotAccessCall(node) { | ||
// The `dotAccess` utility accesses nested fields in the form | ||
// `dotAccess(thing, 'field.nestedField')`. | ||
const pathArg = node.arguments[1]; | ||
if (isStringNode(pathArg)) { | ||
pathArg.value.split('.').forEach(element => { | ||
foundMemberAccesses[element] = true; | ||
pathArg.elements.forEach(element => { | ||
if (isStringNode(element)) { | ||
foundMemberAccesses[element.value] = true; | ||
} | ||
}); | ||
} | ||
} | ||
function visitMemberExpression(node) { | ||
if (node.property.type === 'Identifier') { | ||
foundMemberAccesses[node.property.name] = true; | ||
function visitDotAccessCall(node) { | ||
// The `dotAccess` utility accesses nested fields in the form | ||
// `dotAccess(thing, 'field.nestedField')`. | ||
const pathArg = node.arguments[1]; | ||
if (isStringNode(pathArg)) { | ||
pathArg.value.split('.').forEach(element => { | ||
foundMemberAccesses[element] = true; | ||
}); | ||
} | ||
} | ||
} | ||
return { | ||
Program(_node) { | ||
currentMethod = []; | ||
foundMemberAccesses = {}; | ||
templateLiterals = []; | ||
}, | ||
'Program:exit'(_node) { | ||
templateLiterals.forEach(templateLiteral => { | ||
const graphQLAst = getGraphQLAST(templateLiteral); | ||
if (!graphQLAst) { | ||
// ignore nodes with syntax errors, they're handled by rule-graphql-syntax | ||
function visitMemberExpression(node) { | ||
if (node.property.type === 'Identifier') { | ||
foundMemberAccesses[node.property.name] = true; | ||
} | ||
} | ||
return { | ||
Program(_node) { | ||
currentMethod = []; | ||
foundMemberAccesses = {}; | ||
templateLiterals = []; | ||
}, | ||
'Program:exit'(_node) { | ||
templateLiterals.forEach(templateLiteral => { | ||
const graphQLAst = getGraphQLAST(templateLiteral); | ||
if (!graphQLAst) { | ||
// ignore nodes with syntax errors, they're handled by rule-graphql-syntax | ||
return; | ||
} | ||
const queriedFields = getGraphQLFieldNames(graphQLAst); | ||
for (const field in queriedFields) { | ||
if ( | ||
!foundMemberAccesses[field] && | ||
!isPageInfoField(field) && | ||
// Do not warn for unused __typename which can be a workaround | ||
// when only interested in existence of an object. | ||
field !== '__typename' | ||
) { | ||
context.report({ | ||
node: templateLiteral, | ||
loc: getLoc(context, templateLiteral, queriedFields[field]), | ||
message: | ||
`This queries for the field \`${field}\` but this file does ` + | ||
'not seem to use it directly. If a different file needs this ' + | ||
'information that file should export a fragment and colocate ' + | ||
'the query for the data with the usage.\n' + | ||
'If only interested in the existence of a record, __typename ' + | ||
'can be used without this warning.' | ||
}); | ||
} | ||
} | ||
}); | ||
}, | ||
CallExpression(node) { | ||
if (node.callee.type !== 'Identifier') { | ||
return; | ||
} | ||
const queriedFields = getGraphQLFieldNames(graphQLAst); | ||
for (const field in queriedFields) { | ||
if ( | ||
!foundMemberAccesses[field] && | ||
!isPageInfoField(field) && | ||
// Do not warn for unused __typename which can be a workaround | ||
// when only interested in existence of an object. | ||
field !== '__typename' | ||
) { | ||
context.report({ | ||
node: templateLiteral, | ||
loc: getLoc(context, templateLiteral, queriedFields[field]), | ||
message: | ||
`This queries for the field \`${field}\` but this file does ` + | ||
'not seem to use it directly. If a different file needs this ' + | ||
'information that file should export a fragment and colocate ' + | ||
'the query for the data with the usage.\n' + | ||
'If only interested in the existence of a record, __typename ' + | ||
'can be used without this warning.' | ||
}); | ||
switch (node.callee.name) { | ||
case 'getByPath': | ||
visitGetByPathCall(node); | ||
break; | ||
case 'dotAccess': | ||
visitDotAccessCall(node); | ||
break; | ||
} | ||
}, | ||
TaggedTemplateExpression(node) { | ||
if (currentMethod[0] === 'getConfigs') { | ||
return; | ||
} | ||
if (isGraphQLTemplate(node)) { | ||
templateLiterals.push(node); | ||
} | ||
}, | ||
MemberExpression: visitMemberExpression, | ||
OptionalMemberExpression: visitMemberExpression, | ||
ObjectPattern(node) { | ||
node.properties.forEach(node => { | ||
if (node.type === 'Property' && !node.computed) { | ||
foundMemberAccesses[node.key.name] = true; | ||
} | ||
} | ||
}); | ||
}, | ||
CallExpression(node) { | ||
if (node.callee.type !== 'Identifier') { | ||
return; | ||
}); | ||
}, | ||
MethodDefinition(node) { | ||
currentMethod.unshift(node.key.name); | ||
}, | ||
'MethodDefinition:exit'(_node) { | ||
currentMethod.shift(); | ||
} | ||
switch (node.callee.name) { | ||
case 'getByPath': | ||
visitGetByPathCall(node); | ||
break; | ||
case 'dotAccess': | ||
visitDotAccessCall(node); | ||
break; | ||
} | ||
}, | ||
TaggedTemplateExpression(node) { | ||
if (currentMethod[0] === 'getConfigs') { | ||
return; | ||
} | ||
if (isGraphQLTemplate(node)) { | ||
templateLiterals.push(node); | ||
} | ||
}, | ||
MemberExpression: visitMemberExpression, | ||
OptionalMemberExpression: visitMemberExpression, | ||
ObjectPattern(node) { | ||
node.properties.forEach(node => { | ||
if (node.type === 'Property' && !node.computed) { | ||
foundMemberAccesses[node.key.name] = true; | ||
} | ||
}); | ||
}, | ||
MethodDefinition(node) { | ||
currentMethod.unshift(node.key.name); | ||
}, | ||
'MethodDefinition:exit'(_node) { | ||
currentMethod.shift(); | ||
} | ||
}; | ||
} | ||
module.exports = rule; | ||
}; | ||
} | ||
}; |
@@ -34,3 +34,3 @@ /** | ||
return parse(quasi.value.cooked); | ||
} catch (error) { | ||
} catch (_error) { | ||
// Invalid syntax, covered by graphql-syntax rule | ||
@@ -49,4 +49,7 @@ return null; | ||
return { | ||
start: getLocFromIndex(context.getSourceCode(), start), | ||
end: getLocFromIndex(context.getSourceCode(), end) | ||
start: getLocFromIndex( | ||
context.sourceCode ?? context.getSourceCode(), | ||
start | ||
), | ||
end: getLocFromIndex(context.sourceCode ?? context.getSourceCode(), end) | ||
}; | ||
@@ -109,8 +112,6 @@ } | ||
function isGraphQLDeprecatedTag(tag) { | ||
return tag.type === 'Identifier' && tag.name === 'graphql_DEPRECATED'; | ||
} | ||
function shouldLint(context) { | ||
return /graphql|relay/i.test(context.getSourceCode().text); | ||
return /graphql|relay/i.test( | ||
(context.sourceCode ?? context.getSourceCode()).text | ||
); | ||
} | ||
@@ -132,4 +133,3 @@ | ||
isGraphQLTag: isGraphQLTag, | ||
isGraphQLDeprecatedTag: isGraphQLDeprecatedTag, | ||
shouldLint: shouldLint | ||
}; |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
82
17.14%65449
-2.02%11
83.33%13
-7.14%1857
-4.13%1
Infinity%+ Added
- Removed