Skip to content

Commit

Permalink
feat(modules): Support node_modules paths in graphql imports (#216)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
SpaceK33z committed Sep 7, 2018
1 parent e43898c commit c1385af
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 7 deletions.
6 changes: 6 additions & 0 deletions fixtures/import-module/a.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# import B from 'graphql-import-test/b.graphql'

type A {
id: ID!
author: B!
}
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -53,6 +54,7 @@
"typescript": "3.0.1"
},
"dependencies": {
"lodash": "^4.17.4"
"lodash": "^4.17.4",
"resolve-from": "^4.0.0"
}
}
41 changes: 41 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import test from 'ava'
import * as fs from 'fs'
import { parseImportLine, parseSDL, importSchema } from '.'

test('parseImportLine: parse single import', t => {
Expand Down Expand Up @@ -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`), [])
})
Expand All @@ -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 {
Expand Down
31 changes: 25 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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),
Expand Down

0 comments on commit c1385af

Please sign in to comment.