From 4247b02ff905986c627f1f9d7fa08e90b8564f51 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Thu, 12 Aug 2021 16:33:00 +0300 Subject: [PATCH] feat(json-file-loader): support glob expressions --- .changeset/soft-jokes-run.md | 5 ++ packages/loaders/json-file/src/index.ts | 76 ++++++++++++++----- .../loaders/json-file/tests/loader.spec.ts | 7 +- .../test-files/{ => failing}/malformed.json | 0 4 files changed, 67 insertions(+), 21 deletions(-) create mode 100644 .changeset/soft-jokes-run.md rename packages/loaders/json-file/tests/test-files/{ => failing}/malformed.json (100%) diff --git a/.changeset/soft-jokes-run.md b/.changeset/soft-jokes-run.md new file mode 100644 index 00000000000..44ed33b41bd --- /dev/null +++ b/.changeset/soft-jokes-run.md @@ -0,0 +1,5 @@ +--- +'@graphql-tools/json-file-loader': minor +--- + +feat(json-file-loader): support glob expressions diff --git a/packages/loaders/json-file/src/index.ts b/packages/loaders/json-file/src/index.ts index aef595fe62b..4ff663e7fc0 100644 --- a/packages/loaders/json-file/src/index.ts +++ b/packages/loaders/json-file/src/index.ts @@ -1,7 +1,11 @@ -import { Source, parseGraphQLJSON, Loader, isValidPath, BaseLoaderOptions } from '@graphql-tools/utils'; +import type { GlobbyOptions } from 'globby'; + +import { Source, Loader, isValidPath, BaseLoaderOptions, asArray, parseGraphQLJSON } from '@graphql-tools/utils'; import { isAbsolute, resolve } from 'path'; import { readFileSync, promises as fsPromises, existsSync } from 'fs'; -import { cwd } from 'process'; +import { cwd as processCwd } from 'process'; +import globby from 'globby'; +import unixify from 'unixify'; const { readFile, access } = fsPromises; @@ -12,6 +16,12 @@ const FILE_EXTENSIONS = ['.json']; */ export interface JsonFileLoaderOptions extends BaseLoaderOptions {} +function createGlobbyOptions(options: JsonFileLoaderOptions): GlobbyOptions { + return { absolute: true, ...options, ignore: [] }; +} + +const buildIgnoreGlob = (path: string) => `!${path}`; + /** * This loader loads documents and type definitions from JSON files. * @@ -39,7 +49,7 @@ export class JsonFileLoader implements Loader { async canLoad(pointer: string, options: JsonFileLoaderOptions): Promise { if (isValidPath(pointer)) { if (FILE_EXTENSIONS.find(extension => pointer.endsWith(extension))) { - const normalizedFilePath = isAbsolute(pointer) ? pointer : resolve(options.cwd || cwd(), pointer); + const normalizedFilePath = isAbsolute(pointer) ? pointer : resolve(options.cwd || processCwd(), pointer); try { await access(normalizedFilePath); return true; @@ -55,37 +65,63 @@ export class JsonFileLoader implements Loader { canLoadSync(pointer: string, options: JsonFileLoaderOptions): boolean { if (isValidPath(pointer)) { if (FILE_EXTENSIONS.find(extension => pointer.endsWith(extension))) { - const normalizedFilePath = isAbsolute(pointer) ? pointer : resolve(options.cwd || cwd(), pointer); + const normalizedFilePath = isAbsolute(pointer) ? pointer : resolve(options.cwd || processCwd(), pointer); return existsSync(normalizedFilePath); } } - return false; } + private _buildGlobs(glob: string, options: JsonFileLoaderOptions) { + const ignores = asArray(options.ignore || []); + const globs = [unixify(glob), ...ignores.map(v => buildIgnoreGlob(unixify(v)))]; + return globs; + } + + async resolveGlobs(glob: string, options: JsonFileLoaderOptions) { + const globs = this._buildGlobs(glob, options); + const result = await globby(globs, createGlobbyOptions(options)); + return result; + } + + resolveGlobsSync(glob: string, options: JsonFileLoaderOptions) { + const globs = this._buildGlobs(glob, options); + const result = globby.sync(globs, createGlobbyOptions(options)); + return result; + } + async load(pointer: string, options: JsonFileLoaderOptions): Promise { - const normalizedFilePath = isAbsolute(pointer) ? pointer : resolve(options.cwd || cwd(), pointer); - if (!(await this.canLoad(normalizedFilePath, options))) { - return []; - } + const resolvedPaths = await this.resolveGlobs(pointer, options); + const finalResult: Source[] = []; - try { - const jsonContent: string = await readFile(normalizedFilePath, { encoding: 'utf8' }); - return [parseGraphQLJSON(pointer, jsonContent, options)]; - } catch (e) { - throw new Error(`Unable to read JSON file: ${normalizedFilePath}: ${e.message || /* istanbul ignore next */ e}`); - } + await Promise.all( + resolvedPaths.map(async path => { + if (await this.canLoad(path, options)) { + const normalizedFilePath = isAbsolute(path) ? path : resolve(options.cwd || processCwd(), path); + const rawSDL: string = await readFile(normalizedFilePath, { encoding: 'utf8' }); + finalResult.push(this.handleFileContent(normalizedFilePath, rawSDL, options)); + } + }) + ); + return finalResult; } loadSync(pointer: string, options: JsonFileLoaderOptions): Source[] { - const normalizedFilePath = isAbsolute(pointer) ? pointer : resolve(options.cwd || cwd(), pointer); - if (!this.canLoadSync(normalizedFilePath, options)) { - return []; + const resolvedPaths = this.resolveGlobsSync(pointer, options); + const finalResult: Source[] = []; + for (const path of resolvedPaths) { + if (this.canLoadSync(path, options)) { + const normalizedFilePath = isAbsolute(path) ? path : resolve(options.cwd || processCwd(), path); + const rawSDL = readFileSync(normalizedFilePath, { encoding: 'utf8' }); + finalResult.push(this.handleFileContent(normalizedFilePath, rawSDL, options)); + } } + return finalResult; + } + handleFileContent(normalizedFilePath: string, rawSDL: string, options: JsonFileLoaderOptions): Source { try { - const jsonContent = readFileSync(normalizedFilePath, 'utf8'); - return [parseGraphQLJSON(pointer, jsonContent, options)]; + return parseGraphQLJSON(normalizedFilePath, rawSDL, options); } catch (e) { throw new Error(`Unable to read JSON file: ${normalizedFilePath}: ${e.message || /* istanbul ignore next */ e}`); } diff --git a/packages/loaders/json-file/tests/loader.spec.ts b/packages/loaders/json-file/tests/loader.spec.ts index 1495b115cb3..a9db6a7c804 100644 --- a/packages/loaders/json-file/tests/loader.spec.ts +++ b/packages/loaders/json-file/tests/loader.spec.ts @@ -56,8 +56,13 @@ describe('JsonFileLoader', () => { expect(result.document).toBeDefined(); }); + it('should load multiple files from glob expression', async () => { + const results = await load(join(process.cwd(), getPointer('*.json')), {}); + expect(results).toHaveLength(2); + }) + it('should throw when the file content is malformed', async () => { - await expect(load(getPointer('malformed.json'), {})).rejects.toThrowError('Unable to read JSON file'); + await expect(load(getPointer('failing/malformed.json'), {})).rejects.toThrowError('Unable to read JSON file'); }); it('should skip file it cannot load', async () => { const result = await load(getPointer('id_do_not_exist.json'), {}); diff --git a/packages/loaders/json-file/tests/test-files/malformed.json b/packages/loaders/json-file/tests/test-files/failing/malformed.json similarity index 100% rename from packages/loaders/json-file/tests/test-files/malformed.json rename to packages/loaders/json-file/tests/test-files/failing/malformed.json