Skip to content

Commit 3701b2a

Browse files
author
Dimitri POSTOLOV
authored
1️⃣ Add multi-project support in .eslintrc file, use graphql-config even there is no graphql-config cosmiconfig file (#545)
1 parent 29b61f6 commit 3701b2a

15 files changed

+1535
-400
lines changed

.changeset/gold-mice-sell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-eslint/eslint-plugin': patch
3+
---
4+
5+
use `graphql-config` even there is no `graphql-config` consmiconfig file

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"transpile-ts": "tsc --project tsconfig.json",
1919
"build": "yarn transpile-ts && bob build",
2020
"postbuild": "cp -r README.md docs ./packages/plugin/dist/",
21-
"test": "jest --no-watchman --forceExit",
21+
"test": "jest --no-watchman --forceExit --noStackTrace",
2222
"prerelease": "yarn build",
2323
"release": "changeset publish",
2424
"release:canary": "(node scripts/canary-release.js && yarn build && yarn changeset publish --tag alpha) || echo Skipping Canary...",

packages/plugin/package.json

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,18 @@
1616
"prepack": "bob prepack"
1717
},
1818
"dependencies": {
19-
"@graphql-tools/code-file-loader": "~6.3.0",
20-
"@graphql-tools/graphql-file-loader": "~6.2.0",
21-
"@graphql-tools/graphql-tag-pluck": "~6.5.0",
19+
"@graphql-tools/code-file-loader": "^7.0.1",
20+
"@graphql-tools/graphql-tag-pluck": "^7.0.1",
2221
"@graphql-tools/import": "^6.3.1",
23-
"@graphql-tools/json-file-loader": "~6.2.6",
24-
"@graphql-tools/load": "~6.2.0",
25-
"@graphql-tools/url-loader": "~6.10.0",
26-
"@graphql-tools/utils": "~7.10.0",
27-
"graphql-config": "^3.2.0",
22+
"@graphql-tools/utils": "^8.0.1",
23+
"graphql-config": "^3.4.0",
2824
"graphql-depth-limit": "1.1.0"
2925
},
3026
"devDependencies": {
3127
"@types/eslint": "7.28.0",
3228
"@types/graphql-depth-limit": "1.1.2",
3329
"bob-the-bundler": "1.2.1",
34-
"graphql": "15.5.1",
35-
"graphql-config": "3.4.0",
30+
"graphql": "15.5.0",
3631
"typescript": "4.3.5"
3732
},
3833
"peerDependencies": {

packages/plugin/src/graphql-config.ts

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,46 @@
11
import { GraphQLConfig, GraphQLExtensionDeclaration, loadConfigSync } from 'graphql-config';
2-
import { schemaLoaders } from './schema';
3-
import { operationsLoaders } from './sibling-operations';
2+
import { CodeFileLoader } from '@graphql-tools/code-file-loader';
43
import { ParserOptions } from './types';
54

6-
export function loadGraphqlConfig(options: ParserOptions): GraphQLConfig | null {
7-
if (options?.skipGraphQLConfig) return null;
8-
if (!graphqlConfig) {
9-
graphqlConfig = loadConfigSync({
10-
throwOnEmpty: false,
11-
throwOnMissing: false,
12-
extensions: [addCodeFileLoaderExtension],
13-
});
5+
let graphqlConfig: GraphQLConfig;
6+
7+
export function loadGraphqlConfig(options: ParserOptions): GraphQLConfig {
8+
// We don't want cache config on test environment
9+
// Otherwise schema and documents will be same for all tests
10+
if (process.env.NODE_ENV !== 'test' && graphqlConfig) {
11+
return graphqlConfig;
1412
}
1513

14+
const onDiskConfig = options.skipGraphQLConfig
15+
? null
16+
: loadConfigSync({
17+
throwOnEmpty: false,
18+
throwOnMissing: false,
19+
extensions: [addCodeFileLoaderExtension],
20+
});
21+
22+
graphqlConfig =
23+
onDiskConfig ||
24+
new GraphQLConfig(
25+
{
26+
config: {
27+
schema: options.schema || '', // if undefined will throw error `Project 'default' not found`
28+
documents: options.documents || options.operations,
29+
extensions: options.extensions,
30+
include: options.include,
31+
exclude: options.exclude,
32+
projects: options.projects,
33+
},
34+
filepath: 'virtual-config',
35+
},
36+
[addCodeFileLoaderExtension]
37+
);
38+
1639
return graphqlConfig;
1740
}
1841

19-
let graphqlConfig: GraphQLConfig | null;
20-
2142
const addCodeFileLoaderExtension: GraphQLExtensionDeclaration = api => {
22-
schemaLoaders.forEach(loader => api.loaders.schema.register(loader));
23-
operationsLoaders.forEach(loader => api.loaders.documents.register(loader));
43+
api.loaders.schema.register(new CodeFileLoader());
44+
api.loaders.documents.register(new CodeFileLoader());
2445
return { name: 'graphql-eslint-loaders' };
2546
};

packages/plugin/src/rules/no-unused-fields.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { GraphQLESLintRule } from '../types';
22
import { requireUsedFieldsFromContext } from '../utils';
33

44
const UNUSED_FIELD = 'UNUSED_FIELD';
5-
const ruleName = 'no-unused-fields';
5+
const RULE_NAME = 'no-unused-fields';
66

77
const rule: GraphQLESLintRule = {
88
meta: {
@@ -12,7 +12,7 @@ const rule: GraphQLESLintRule = {
1212
docs: {
1313
description: `Requires all fields to be used at some level by siblings operations.`,
1414
category: 'Best Practices',
15-
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${ruleName}.md`,
15+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_NAME}.md`,
1616
requiresSiblings: true,
1717
requiresSchema: true,
1818
examples: [
@@ -28,7 +28,7 @@ const rule: GraphQLESLintRule = {
2828
type Query {
2929
me: User
3030
}
31-
31+
3232
query {
3333
me {
3434
id
@@ -48,7 +48,7 @@ const rule: GraphQLESLintRule = {
4848
type Query {
4949
me: User
5050
}
51-
51+
5252
query {
5353
me {
5454
id
@@ -63,14 +63,12 @@ const rule: GraphQLESLintRule = {
6363
type: 'suggestion',
6464
},
6565
create(context) {
66-
const sourceCode = context.getSourceCode();
67-
const usedFields = requireUsedFieldsFromContext(ruleName, context);
66+
const usedFields = requireUsedFieldsFromContext(RULE_NAME, context);
6867

6968
return {
7069
FieldDefinition(node) {
7170
const fieldName = node.name.value;
7271
const parentTypeName = (node as any).parent.name.value;
73-
7472
const isUsed = usedFields[parentTypeName]?.has(fieldName);
7573

7674
if (isUsed) {
@@ -82,6 +80,7 @@ const rule: GraphQLESLintRule = {
8280
messageId: UNUSED_FIELD,
8381
data: { fieldName },
8482
fix(fixer) {
83+
const sourceCode = context.getSourceCode();
8584
const tokenBefore = (sourceCode as any).getTokenBefore(node);
8685
const tokenAfter = (sourceCode as any).getTokenAfter(node);
8786
const isEmptyType = tokenBefore.type === '{' && tokenAfter.type === '}';

packages/plugin/src/schema.ts

Lines changed: 15 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,28 @@
1-
import { CodeFileLoader } from '@graphql-tools/code-file-loader';
2-
import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader';
3-
import { JsonFileLoader } from '@graphql-tools/json-file-loader';
4-
import { loadSchemaSync } from '@graphql-tools/load';
5-
import { UrlLoader } from '@graphql-tools/url-loader';
6-
import { Loader, SingleFileOptions } from '@graphql-tools/utils';
7-
import { buildSchema, GraphQLSchema } from 'graphql';
1+
import { GraphQLSchema } from 'graphql';
82
import { GraphQLConfig } from 'graphql-config';
9-
import { dirname } from 'path';
3+
import { asArray } from '@graphql-tools/utils';
104
import { ParserOptions } from './types';
115
import { getOnDiskFilepath } from './utils';
126

137
const schemaCache: Map<string, GraphQLSchema> = new Map();
148

15-
export const schemaLoaders: Loader<string, SingleFileOptions>[] = [
16-
{
17-
loaderId: () => 'direct-string',
18-
canLoad: async () => false,
19-
load: async () => null,
20-
canLoadSync: pointer => typeof pointer === 'string' && pointer.includes('type '),
21-
loadSync: pointer => ({
22-
schema: buildSchema(pointer),
23-
}),
24-
},
25-
new GraphQLFileLoader(),
26-
new JsonFileLoader(),
27-
new UrlLoader(),
28-
new CodeFileLoader(),
29-
];
30-
319
export function getSchema(options: ParserOptions, gqlConfig: GraphQLConfig): GraphQLSchema | null {
32-
let schema: GraphQLSchema | null = null;
33-
34-
// We first try to use graphql-config for loading the schema, based on the type of the file,
35-
// We are using the directory of the file as the key for the schema caching, to avoid reloading of the schema.
36-
if (gqlConfig && options.filePath) {
37-
const realFilepath = getOnDiskFilepath(options.filePath);
38-
const projectForFile = gqlConfig.getProjectForFile(realFilepath);
39-
const schemaKey = projectForFile.schema.toString();
40-
41-
if (schemaCache.has(schemaKey)) {
42-
schema = schemaCache.get(schemaKey);
43-
} else {
44-
schema = projectForFile.getSchemaSync();
45-
schemaCache.set(schemaKey, schema);
46-
}
10+
const realFilepath = options.filePath ? getOnDiskFilepath(options.filePath) : null;
11+
const projectForFile = realFilepath ? gqlConfig.getProjectForFile(realFilepath) : gqlConfig.getDefault();
12+
const schemaKey = asArray(projectForFile.schema)
13+
.sort()
14+
.join(',');
15+
16+
if (!schemaKey) {
17+
return null;
4718
}
4819

49-
// If schema was not loaded yet, and user configured it in the parserConfig, we can try to load it,
50-
// In this case, the cache key is the path for the schema. This is needed in order to allow separate
51-
// configurations for different file paths (a very edgey case).
52-
if (!schema && options.schema) {
53-
const schemaKey = options.schema.toString();
54-
55-
if (schemaCache.has(schemaKey)) {
56-
schema = schemaCache.get(schemaKey);
57-
} else {
58-
try {
59-
schema = loadSchemaSync(options.schema, {
60-
...(options.schemaOptions || {}),
61-
assumeValidSDL: true,
62-
loaders: schemaLoaders,
63-
});
64-
schemaCache.set(schemaKey, schema);
65-
} catch (e) {
66-
e.message += `\nRunning from directory: ${process.cwd()}`;
67-
68-
throw e;
69-
}
70-
}
20+
if (schemaCache.has(schemaKey)) {
21+
return schemaCache.get(schemaKey);
7122
}
7223

24+
const schema = projectForFile.getSchemaSync();
25+
schemaCache.set(schemaKey, schema);
26+
7327
return schema;
7428
}

packages/plugin/src/sibling-operations.ts

Lines changed: 32 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,19 @@
1-
import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader';
2-
import { CodeFileLoader } from '@graphql-tools/code-file-loader';
3-
import { loadDocumentsSync } from '@graphql-tools/load';
4-
import { Loader, SingleFileOptions, Source } from '@graphql-tools/utils';
51
import {
62
FragmentDefinitionNode,
73
FragmentSpreadNode,
84
Kind,
95
OperationDefinitionNode,
10-
parse,
116
SelectionSetNode,
127
visit,
138
} from 'graphql';
14-
import { ParserOptions } from './types';
9+
import { Source, asArray } from '@graphql-tools/utils';
1510
import { GraphQLConfig } from 'graphql-config';
16-
import { dirname } from 'path';
11+
import { ParserOptions } from './types';
12+
import { getOnDiskFilepath } from './utils';
1713

1814
export type FragmentSource = { filePath: string; document: FragmentDefinitionNode };
1915
export type OperationSource = { filePath: string; document: OperationDefinitionNode };
2016

21-
export const operationsLoaders: Loader<string, SingleFileOptions>[] = [
22-
new GraphQLFileLoader(),
23-
new CodeFileLoader(),
24-
{
25-
loaderId: () => 'direct-string',
26-
canLoad: async () => false,
27-
load: async () => null,
28-
canLoadSync: pointer => typeof pointer === 'string' && pointer.includes('type '),
29-
loadSync: pointer => ({
30-
document: parse(pointer),
31-
}),
32-
},
33-
];
34-
3517
export type SiblingOperations = {
3618
available: boolean;
3719
getOperations(): OperationSource[];
@@ -46,65 +28,46 @@ export type SiblingOperations = {
4628
getOperationByType(operationType: 'query' | 'mutation' | 'subscription'): OperationSource[];
4729
};
4830

49-
function loadSiblings(baseDir: string, loadPaths: string[]): Source[] {
50-
return loadDocumentsSync(loadPaths, {
51-
cwd: baseDir,
52-
loaders: operationsLoaders,
53-
skipGraphQLImport: true,
54-
});
55-
}
56-
5731
const operationsCache: Map<string, Source[]> = new Map();
5832
const siblingOperationsCache: Map<Source[], SiblingOperations> = new Map();
5933

60-
export function getSiblingOperations(options: ParserOptions, gqlConfig: GraphQLConfig): SiblingOperations {
61-
let siblings: Source[] | null = null;
62-
63-
// We first try to use graphql-config for loading the operations paths, based on the type of the file,
64-
// We are using the directory of the file as the key for the schema caching, to avoid reloading of the schema.
65-
if (gqlConfig && options?.filePath) {
66-
const fileDir = dirname(options.filePath);
67-
68-
if (operationsCache.has(fileDir)) {
69-
siblings = operationsCache.get(fileDir);
70-
} else {
71-
const projectForFile = gqlConfig.getProjectForFile(options.filePath);
72-
73-
if (projectForFile?.documents) {
74-
siblings = projectForFile.loadDocumentsSync(projectForFile.documents, {
75-
skipGraphQLImport: true,
76-
});
77-
operationsCache.set(fileDir, siblings);
78-
}
79-
}
80-
}
34+
const getSiblings = (filePath: string, gqlConfig: GraphQLConfig): Source[] | null => {
35+
const realFilepath = filePath ? getOnDiskFilepath(filePath) : null;
36+
const projectForFile = realFilepath ? gqlConfig.getProjectForFile(realFilepath) : gqlConfig.getDefault();
37+
const documentsKey = asArray(projectForFile.documents)
38+
.sort()
39+
.join(',');
8140

82-
if (!siblings && options?.operations) {
83-
const loadPaths = Array.isArray(options.operations) ? options.operations : [options.operations];
84-
const loadKey = loadPaths.join(',');
41+
if (!documentsKey) {
42+
return [];
43+
}
8544

86-
if (operationsCache.has(loadKey)) {
87-
siblings = operationsCache.get(loadKey);
88-
} else {
89-
siblings = loadSiblings(process.cwd(), loadPaths);
90-
operationsCache.set(loadKey, siblings);
91-
}
45+
if (operationsCache.has(documentsKey)) {
46+
return operationsCache.get(documentsKey);
9247
}
9348

94-
if (!siblings || siblings.length === 0) {
49+
const siblings = projectForFile.loadDocumentsSync(projectForFile.documents, {
50+
skipGraphQLImport: true,
51+
});
52+
operationsCache.set(documentsKey, siblings);
53+
54+
return siblings;
55+
};
56+
57+
export function getSiblingOperations(options: ParserOptions, gqlConfig: GraphQLConfig): SiblingOperations {
58+
const siblings = getSiblings(options.filePath, gqlConfig);
59+
60+
if (siblings.length === 0) {
9561
let printed = false;
9662

9763
const noopWarn = () => {
98-
if (printed) {
99-
return [];
64+
if (!printed) {
65+
// eslint-disable-next-line no-console
66+
console.warn(
67+
`getSiblingOperations was called without any operations. Make sure to set "parserOptions.operations" to make this feature available!`
68+
);
69+
printed = true;
10070
}
101-
102-
printed = true;
103-
// eslint-disable-next-line no-console
104-
console.warn(
105-
`getSiblingOperations was called without any operations. Make sure to set "parserOptions.operations" to make this feature available!`
106-
);
107-
10871
return [];
10972
};
11073

packages/plugin/src/testkit.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export type GraphQLInvalidTestCase<T> = GraphQLValidTestCase<T> & {
2121
};
2222

2323
export class GraphQLRuleTester extends require('eslint').RuleTester {
24-
constructor(parserOptions?: ParserOptions) {
24+
constructor(parserOptions: ParserOptions = {}) {
2525
super({
2626
parser: require.resolve('@graphql-eslint/eslint-plugin'),
2727
parserOptions: {

0 commit comments

Comments
 (0)