Skip to content

Commit

Permalink
Import GraphQL files from other GraphQL files (closes #1477) (#1892)
Browse files Browse the repository at this point in the history
An alternative to #1817 that doesn't rely on using `graphql-tag/loader`, which is a Webpack loader.

#### What this does:
1. Recursively traverse the GraphQL files.
2. Collect all the imports into a map.
3. Concatenate them.
4. Parse the result into an AST.

#### What this doesn't do:

This PR (as of time of writing) does not have feature parity with `graphql-tag/loader`, meaning that:
* It does not de-duplicate fragments that have the same name. _This is a behavior that I personally disapprove of since it can lead to surprising results instead of throwing an error if a user accidentally (however unlikely) reuses an already defined fragment name._
* It does not collect multiple defined queries/mutations and make them individually require-able from a JavaScript parent.


If the above behaviours are desired then I can implement them.
  • Loading branch information
arianon authored and DeMoorJasper committed Aug 14, 2018
1 parent 81151bb commit afbca87
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 1 deletion.
49 changes: 48 additions & 1 deletion src/assets/GraphqlAsset.js
@@ -1,15 +1,62 @@
const Asset = require('../Asset');
const localRequire = require('../utils/localRequire');
const Resolver = require('../Resolver');
const fs = require('../utils/fs');
const os = require('os');

const IMPORT_RE = /^# *import +['"](.*)['"] *;? *$/;

class GraphqlAsset extends Asset {
constructor(name, options) {
super(name, options);
this.type = 'js';

this.gqlMap = new Map();
this.gqlResolver = new Resolver(
Object.assign({}, this.options, {
extensions: ['.gql', '.graphql']
})
);
}

async traverseImports(name, code) {
this.gqlMap.set(name, code);

await Promise.all(
code
.split(/\r\n?|\n/)
.map(line => line.match(IMPORT_RE))
.filter(match => !!match)
.map(async ([, importName]) => {
let {path: resolved} = await this.gqlResolver.resolve(
importName,
name
);

if (this.gqlMap.has(resolved)) {
return;
}

let code = await fs.readFile(resolved, 'utf8');
await this.traverseImports(resolved, code);
})
);
}

collectDependencies() {
for (let [path] of this.gqlMap) {
this.addDependency(path, {includedInParent: true});
}
}

async parse(code) {
let gql = await localRequire('graphql-tag', this.name);
return gql(code);

await this.traverseImports(this.name, code);

const allCodes = [...this.gqlMap.values()].join(os.EOL);

return gql(allCodes);
}

generate() {
Expand Down
39 changes: 39 additions & 0 deletions test/graphql.js
Expand Up @@ -35,4 +35,43 @@ describe('graphql', function() {
`.definitions
);
});

it('should support importing other graphql files from a graphql file', async function() {
let b = await bundle(__dirname + '/integration/graphql-import/index.js');

await assertBundleTree(b, {
name: 'index.js',
assets: ['index.js', 'local.graphql'],
childBundles: [
{
type: 'map'
}
]
});

let output = await run(b);
assert.equal(typeof output, 'function');
assert.deepEqual(
output().definitions,
gql`
{
user(id: 6) {
...UserFragment
...AnotherUserFragment
}
}
fragment UserFragment on User {
firstName
lastName
}
fragment AnotherUserFragment on User {
address
email
}
`.definitions
);
});
});
4 changes: 4 additions & 0 deletions test/integration/graphql-import/another.graphql
@@ -0,0 +1,4 @@
fragment AnotherUserFragment on User {
address
email
}
5 changes: 5 additions & 0 deletions test/integration/graphql-import/index.js
@@ -0,0 +1,5 @@
var local = require('./local.graphql');

module.exports = function () {
return local;
};
8 changes: 8 additions & 0 deletions test/integration/graphql-import/local.graphql
@@ -0,0 +1,8 @@
# import './other.graphql'

{
user(id: 6) {
...UserFragment
...AnotherUserFragment
}
}
6 changes: 6 additions & 0 deletions test/integration/graphql-import/other.graphql
@@ -0,0 +1,6 @@
# import './another.graphql'

fragment UserFragment on User {
firstName
lastName
}

0 comments on commit afbca87

Please sign in to comment.