From afbca8761261102f94e931636d953e834c26720e Mon Sep 17 00:00:00 2001 From: arianon Date: Tue, 14 Aug 2018 12:11:17 -0400 Subject: [PATCH] Import GraphQL files from other GraphQL files (closes #1477) (#1892) 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. --- src/assets/GraphqlAsset.js | 49 ++++++++++++++++++- test/graphql.js | 39 +++++++++++++++ .../graphql-import/another.graphql | 4 ++ test/integration/graphql-import/index.js | 5 ++ test/integration/graphql-import/local.graphql | 8 +++ test/integration/graphql-import/other.graphql | 6 +++ 6 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 test/integration/graphql-import/another.graphql create mode 100644 test/integration/graphql-import/index.js create mode 100644 test/integration/graphql-import/local.graphql create mode 100644 test/integration/graphql-import/other.graphql diff --git a/src/assets/GraphqlAsset.js b/src/assets/GraphqlAsset.js index 62e7a3563d9..8517e2574f3 100644 --- a/src/assets/GraphqlAsset.js +++ b/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() { diff --git a/test/graphql.js b/test/graphql.js index ed6eacc93af..136eb030d70 100644 --- a/test/graphql.js +++ b/test/graphql.js @@ -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 + ); + }); }); diff --git a/test/integration/graphql-import/another.graphql b/test/integration/graphql-import/another.graphql new file mode 100644 index 00000000000..53656ef91d4 --- /dev/null +++ b/test/integration/graphql-import/another.graphql @@ -0,0 +1,4 @@ +fragment AnotherUserFragment on User { + address + email +} \ No newline at end of file diff --git a/test/integration/graphql-import/index.js b/test/integration/graphql-import/index.js new file mode 100644 index 00000000000..7e39597facb --- /dev/null +++ b/test/integration/graphql-import/index.js @@ -0,0 +1,5 @@ +var local = require('./local.graphql'); + +module.exports = function () { + return local; +}; \ No newline at end of file diff --git a/test/integration/graphql-import/local.graphql b/test/integration/graphql-import/local.graphql new file mode 100644 index 00000000000..63630612f5b --- /dev/null +++ b/test/integration/graphql-import/local.graphql @@ -0,0 +1,8 @@ +# import './other.graphql' + +{ + user(id: 6) { + ...UserFragment + ...AnotherUserFragment + } +} \ No newline at end of file diff --git a/test/integration/graphql-import/other.graphql b/test/integration/graphql-import/other.graphql new file mode 100644 index 00000000000..aea4d154a6c --- /dev/null +++ b/test/integration/graphql-import/other.graphql @@ -0,0 +1,6 @@ +# import './another.graphql' + +fragment UserFragment on User { + firstName + lastName +} \ No newline at end of file