From 4c44f5638fb19ea3a58d6e662b2e79f264a0843d Mon Sep 17 00:00:00 2001 From: Kees Kluskens Date: Fri, 7 Sep 2018 14:34:54 +0200 Subject: [PATCH] Support for node_modules in graphql imports (#216) This is a continuation of PR #136 from @lfades (spoke to @lfades about this and he was okay with it). It uses `resolve-from` instead of `require.resolve` with the `paths` option because it is not compatible with Node < 8. Also I think this one works a bit differently; it doesn't introduce a breaking change since it will first try to look up the path relative to the file the import is in, and only if that fails it will use `resolve-from`. Fixes #57 --- fixtures/import-module/a.graphql | 6 +++++ package.json | 4 +++- src/index.test.ts | 41 ++++++++++++++++++++++++++++++++ src/index.ts | 31 +++++++++++++++++++----- 4 files changed, 75 insertions(+), 7 deletions(-) create mode 100644 fixtures/import-module/a.graphql diff --git a/fixtures/import-module/a.graphql b/fixtures/import-module/a.graphql new file mode 100644 index 0000000..d6b9ce8 --- /dev/null +++ b/fixtures/import-module/a.graphql @@ -0,0 +1,6 @@ +# import B from 'graphql-import-test/b.graphql' + +type A { + id: ID! + author: B! +} diff --git a/package.json b/package.json index d184d46..7aa5e91 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "devDependencies": { "@types/graphql": "0.12.6", "@types/lodash": "4.14.116", + "@types/resolve-from": "0.0.18", "@types/node": "9.6.31", "ava": "0.25.0", "ava-ts": "0.25.1", @@ -53,6 +54,7 @@ "typescript": "3.0.1" }, "dependencies": { - "lodash": "^4.17.4" + "lodash": "^4.17.4", + "resolve-from": "^4.0.0" } } diff --git a/src/index.test.ts b/src/index.test.ts index 4da6512..db867f3 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,4 +1,5 @@ import test from 'ava' +import * as fs from 'fs' import { parseImportLine, parseSDL, importSchema } from '.' test('parseImportLine: parse single import', t => { @@ -44,6 +45,13 @@ test('parseImportLine: different path', t => { }) }) +test('parseImportLine: module in node_modules', t => { + t.deepEqual(parseImportLine(`import A from "module-name"`), { + imports: ['A'], + from: 'module-name', + }) +}) + test('parseSDL: non-import comment', t => { t.deepEqual(parseSDL(`#importent: comment`), []) }) @@ -65,6 +73,39 @@ test('parse: multi line import', t => { ]) }) +test('Module in node_modules', t => { + const b = `\ +# import lower from './lower.graphql' +type B { + id: ID! + nickname: String! @lower +} +` + const lower = `\ +directive @lower on FIELD_DEFINITION +` + const expectedSDL = `\ +type A { + id: ID! + author: B! +} + +type B { + id: ID! + nickname: String! @lower +} + +directive @lower on FIELD_DEFINITION +` + const moduleDir = 'node_modules/graphql-import-test' + if (!fs.existsSync(moduleDir)) { + fs.mkdirSync(moduleDir) + } + fs.writeFileSync(moduleDir + '/b.graphql', b) + fs.writeFileSync(moduleDir + '/lower.graphql', lower) + t.is(importSchema('fixtures/import-module/a.graphql'), expectedSDL) +}) + test('importSchema: imports only', t => { const expectedSDL = `\ type Query { diff --git a/src/index.ts b/src/index.ts index 0646218..ac4838e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,7 @@ import { } from 'graphql' import { flatten, groupBy, includes, keyBy, isEqual } from 'lodash' import * as path from 'path' +import * as resolveFrom from 'resolve-from' import { completeDefinitionPool, ValidDefinitionNode } from './definition' @@ -164,6 +165,29 @@ function isEmptySDL(sdl: string): boolean { ) } +/** + * Resolve the path of an import. + * First it will try to find a file relative from the file the import is in, if that fails it will try to resolve it as a module so imports from packages work correctly. + * + * @param filePath Path the import was made from + * @param importFrom Path given for the import + * @returns Full resolved path to a file + */ +function resolveModuleFilePath(filePath: string, importFrom: string): string { + const dirname = path.dirname(filePath) + if (isFile(filePath) && isFile(importFrom)) { + try { + return fs.realpathSync(path.join(dirname, importFrom)) + } catch (e) { + if (e.code === 'ENOENT') { + return resolveFrom(dirname, importFrom) + } + } + } + + return importFrom +} + /** * Recursively process all schema files. Keeps track of both the filtered * type definitions, and all type definitions, because they might be needed @@ -190,7 +214,6 @@ function collectDefinitions( typeDefinitions: ValidDefinitionNode[][] } { const key = isFile(filePath) ? path.resolve(filePath) : filePath - const dirname = path.dirname(filePath) // Get TypeDefinitionNodes from current schema const document = getDocumentFromSDL(sdl) @@ -214,16 +237,12 @@ function collectDefinitions( // Process each file (recursively) rawModules.forEach(m => { // If it was not yet processed (in case of circular dependencies) - const moduleFilePath = - isFile(filePath) && isFile(m.from) - ? path.resolve(path.join(dirname, m.from)) - : m.from + const moduleFilePath = resolveModuleFilePath(filePath, m.from) const processedFile = processedFiles.get(key) if (!processedFile || !processedFile.find(rModule => isEqual(rModule, m))) { // Mark this specific import line as processed for this file (for cicular dependency cases) processedFiles.set(key, processedFile ? processedFile.concat(m) : [m]) - collectDefinitions( m.imports, read(moduleFilePath, schemas),