🚀 Big News: Socket Acquires Coana to Bring Reachability Analysis to Every Appsec Team.Learn more
Socket
Sign inDemoInstall
Socket

eslint-plugin-relay

Package Overview
Dependencies
Maintainers
6
Versions
60
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

eslint-plugin-relay - npm Package Compare versions

Comparing version

to
2.0.0

src/rule-generated-typescript-types.js

31

eslint-plugin-relay.js

@@ -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'
}
}
}
};

24

package.json
{
"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 [![Build Status](https://travis-ci.org/relayjs/eslint-plugin-relay.svg?branch=master)](https://travis-ci.org/relayjs/eslint-plugin-relay) [![npm version](https://badge.fury.io/js/eslint-plugin-relay.svg)](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
};