Skip to content

Commit efe8b8a

Browse files
authoredMar 9, 2021
feat(load-files): ability to provide a custom extractExports (#2700)
1 parent 2b6f6e9 commit efe8b8a

File tree

5 files changed

+119
-92
lines changed

5 files changed

+119
-92
lines changed
 

‎.changeset/lucky-bats-relax.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-tools/load-files': minor
3+
---
4+
5+
feat(load-files): ability to provide a custom extractExports

‎packages/load-files/src/index.ts

+36-55
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,30 @@ const { readFile, stat } = fsPromises;
77

88
const DEFAULT_IGNORED_EXTENSIONS = ['spec', 'test', 'd', 'map'];
99
const DEFAULT_EXTENSIONS = ['gql', 'graphql', 'graphqls', 'ts', 'js'];
10-
const DEFAULT_EXPORT_NAMES = ['typeDefs', 'schema'];
10+
const DEFAULT_EXPORT_NAMES = ['schema', 'typeDef', 'typeDefs', 'resolver', 'resolvers'];
11+
const DEFAULT_EXTRACT_EXPORTS_FACTORY = (exportNames: string[]) => (fileExport: any): any | null => {
12+
if (!fileExport) {
13+
return null;
14+
}
15+
16+
if (fileExport.default) {
17+
for (const exportName of exportNames) {
18+
if (fileExport.default[exportName]) {
19+
return fileExport.default[exportName];
20+
}
21+
}
22+
23+
return fileExport.default;
24+
}
25+
26+
for (const exportName of exportNames) {
27+
if (fileExport[exportName]) {
28+
return fileExport[exportName];
29+
}
30+
}
31+
32+
return fileExport;
33+
};
1134

1235
function asArray<T>(obj: T | T[]): T[] {
1336
if (obj instanceof Array) {
@@ -56,30 +79,6 @@ function buildGlob(
5679
return `${basePath}${recursive ? '/**' : ''}/${ignored}+(${ext})`;
5780
}
5881

59-
function extractExports(fileExport: any, exportNames: string[]): any | null {
60-
if (!fileExport) {
61-
return null;
62-
}
63-
64-
if (fileExport.default) {
65-
for (const exportName of exportNames) {
66-
if (fileExport.default[exportName]) {
67-
return fileExport.default[exportName];
68-
}
69-
}
70-
71-
return fileExport.default;
72-
}
73-
74-
for (const exportName of exportNames) {
75-
if (fileExport[exportName]) {
76-
return fileExport[exportName];
77-
}
78-
}
79-
80-
return fileExport;
81-
}
82-
8382
/**
8483
* Additional options for loading files
8584
*/
@@ -100,6 +99,8 @@ export interface LoadFilesOptions {
10099
recursive?: boolean;
101100
// Set to `true` to ignore files named `index.js` and `index.ts`
102101
ignoreIndex?: boolean;
102+
// Custom export extractor function
103+
extractExports?: (fileExport: any) => any;
103104
}
104105

105106
const LoadFilesDefaultOptions: LoadFilesOptions = {
@@ -134,6 +135,9 @@ export function loadFilesSync<T = any>(
134135
options.globOptions
135136
);
136137

138+
const extractExports = execOptions.extractExports || DEFAULT_EXTRACT_EXPORTS_FACTORY(execOptions.exportNames);
139+
const requireMethod = execOptions.requireMethod || require;
140+
137141
return relevantPaths
138142
.map(path => {
139143
if (!checkExtension(path, options)) {
@@ -147,33 +151,8 @@ export function loadFilesSync<T = any>(
147151
const extension = extname(path);
148152

149153
if (extension === formatExtension('js') || extension === formatExtension('ts') || execOptions.useRequire) {
150-
const fileExports = (execOptions.requireMethod ? execOptions.requireMethod : require)(path);
151-
const extractedExport = extractExports(fileExports, execOptions.exportNames);
152-
153-
if (extractedExport.typeDefs && extractedExport.resolvers) {
154-
return extractedExport;
155-
}
156-
157-
if (extractedExport.schema) {
158-
return extractedExport.schema;
159-
}
160-
161-
if (extractedExport.typeDef) {
162-
return extractedExport.typeDef;
163-
}
164-
165-
if (extractedExport.typeDefs) {
166-
return extractedExport.typeDefs;
167-
}
168-
169-
if (extractedExport.resolver) {
170-
return extractedExport.resolver;
171-
}
172-
173-
if (extractedExport.resolvers) {
174-
return extractedExport.resolvers;
175-
}
176-
154+
const fileExports = requireMethod(path);
155+
const extractedExport = extractExports(fileExports);
177156
return extractedExport;
178157
} else {
179158
return readFileSync(path, { encoding: 'utf-8' });
@@ -232,7 +211,9 @@ export async function loadFiles(
232211
options.globOptions
233212
);
234213

235-
const require$ = (path: string) => import(path).catch(async () => require(path));
214+
const extractExports = execOptions.extractExports || DEFAULT_EXTRACT_EXPORTS_FACTORY(execOptions.exportNames);
215+
const defaultRequireMethod = (path: string) => import(path).catch(async () => require(path));
216+
const requireMethod = execOptions.requireMethod || defaultRequireMethod;
236217

237218
return Promise.all(
238219
relevantPaths
@@ -241,8 +222,8 @@ export async function loadFiles(
241222
const extension = extname(path);
242223

243224
if (extension === formatExtension('js') || extension === formatExtension('ts') || execOptions.useRequire) {
244-
const fileExports = await (execOptions.requireMethod ? execOptions.requireMethod : require$)(path);
245-
const extractedExport = extractExports(fileExports, execOptions.exportNames);
225+
const fileExports = await requireMethod(path);
226+
const extractedExport = extractExports(fileExports);
246227

247228
if (extractedExport.resolver) {
248229
return extractedExport.resolver;

‎packages/load-files/tests/file-scanner.spec.ts

+45-37
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { loadFilesSync, loadFiles, LoadFilesOptions } from '@graphql-tools/load-files';
22
import { print } from 'graphql';
3+
import { join } from 'path';
4+
5+
const syncAndAsync = Object.entries({ 'SYNC': loadFilesSync, 'ASYNC': loadFiles });
36

47
function testSchemaDir({ path, expected, note, extensions, ignoreIndex }: TestDirOptions) {
58
let options: LoadFilesOptions;
@@ -15,29 +18,22 @@ function testSchemaDir({ path, expected, note, extensions, ignoreIndex }: TestDi
1518
};
1619
});
1720

18-
it(`SYNC: should return the correct schema results for path: ${path} (${note})`, () => {
19-
const result = loadFilesSync(path, options);
20-
21-
expect(result.length).toBe(expected.length);
22-
expect(result.map(res => {
23-
if (res.kind === 'Document') {
24-
res = print(res);
25-
}
26-
return stripWhitespaces(res);
27-
})).toEqual(expected.map(stripWhitespaces));
28-
});
21+
syncAndAsync.forEach(([type, loadFiles]) => {
22+
describe(type, () => {
23+
it(`should return the correct schema results for path: ${path} (${note})`, async () => {
24+
const result = await loadFiles(path, options);
2925

30-
it(`ASYNC: should return the correct schema results for path: ${path} (${note})`, async () => {
31-
const result = await loadFiles(path, options);
26+
expect(result.length).toBe(expected.length);
27+
expect(result.map(res => {
28+
if (res.kind === 'Document') {
29+
res = print(res);
30+
}
31+
return stripWhitespaces(res);
32+
})).toEqual(expected.map(stripWhitespaces));
33+
});
34+
});
35+
})
3236

33-
expect(result.length).toBe(expected.length);
34-
expect(result.map(res => {
35-
if (res.kind === 'Document') {
36-
res = print(res);
37-
}
38-
return stripWhitespaces(res);
39-
})).toEqual(expected.map(stripWhitespaces));
40-
});
4137
}
4238

4339
function testResolversDir({ path, expected, note, extensions, compareValue, ignoreIndex, ignoredExtensions }: TestDirOptions) {
@@ -58,24 +54,18 @@ function testResolversDir({ path, expected, note, extensions, compareValue, igno
5854
};
5955
});
6056

61-
it(`SYNC: should return the correct resolvers results for path: ${path} (${note})`, () => {
62-
const result = loadFilesSync(path, options);
63-
64-
expect(result.length).toBe(expected.length);
65-
66-
if (compareValue) {
67-
expect(result).toEqual(expected);
68-
}
69-
});
70-
71-
it(`ASYNC: should return the correct resolvers results for path: ${path} (${note})`, async () => {
72-
const result = await loadFiles(path, options);
57+
syncAndAsync.forEach(([type, loadFiles]) => {
58+
describe(type, () => {
59+
it(`should return the correct resolvers results for path: ${path} (${note})`, async () => {
60+
const result = await loadFiles(path, options);
7361

74-
expect(result.length).toBe(expected.length);
62+
expect(result.length).toBe(expected.length);
7563

76-
if (compareValue) {
77-
expect(result).toEqual(expected);
78-
}
64+
if (compareValue) {
65+
expect(result).toEqual(expected);
66+
}
67+
});
68+
})
7969
});
8070
}
8171

@@ -228,6 +218,24 @@ describe('file scanner', function() {
228218
note: 'extensions and ignored extensions works with a trailing dot',
229219
});
230220
});
221+
syncAndAsync.forEach(([type, loadFiles]) => {
222+
it(`${type}: should process custom extractExports properly`, async () => {
223+
const customQueryTypeName = 'root_query';
224+
const customExtractExports = (fileExport: any) => {
225+
fileExport = fileExport.default || fileExport;
226+
// Incoming exported value is function
227+
return fileExport(customQueryTypeName);
228+
};
229+
const loadedFiles = await loadFiles(join(__dirname, './test-assets/custom-extractor/factory-func.js'), {
230+
extractExports: customExtractExports
231+
});
232+
expect(loadedFiles).toHaveLength(1);
233+
expect(customQueryTypeName in loadedFiles[0]).toBeTruthy();
234+
expect('foo' in loadedFiles[0][customQueryTypeName]).toBeTruthy();
235+
expect(typeof loadedFiles[0][customQueryTypeName]['foo']).toBe('function');
236+
expect(loadedFiles[0][customQueryTypeName]['foo']()).toBe('FOO');
237+
});
238+
})
231239
});
232240

233241
interface TestDirOptions {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = QueryTypeName => ({
2+
[QueryTypeName]: {
3+
foo: () => 'FOO'
4+
}
5+
});

‎website/docs/schema-merging.md

+28
Original file line numberDiff line numberDiff line change
@@ -414,3 +414,31 @@ Or by **type**...
414414
| | | +-- ...
415415
| | +-- index.ts <<< Merges all `*.resolvers.*` files
416416
```
417+
418+
**Custom extraction from exports**
419+
420+
By default, `loadFiles` checks export names `typeDefs`, `resolvers` and `schema`. But you can change the way it extracts the content from the exported values.
421+
422+
Let's say you have a factory function inside your resolvers like below;
423+
424+
```js
425+
module.exports = customQueryTypeName = ({
426+
[customQueryTypeName]: {
427+
foo: () => 'FOO',
428+
}
429+
})
430+
```
431+
432+
And you can define custom `extractExports` like below;
433+
```js
434+
const { loadFilesSync } = require('@graphql-tools/load-files');
435+
436+
const resolvers = loadFilesSync(join(__dirname, './resolvers/**/*.js'), {
437+
extractExports: fileExport => {
438+
if (typeof fileExport === 'function') {
439+
return fileExport('query_root');
440+
}
441+
return fileExport;
442+
}
443+
})
444+
```

0 commit comments

Comments
 (0)
Please sign in to comment.