diff --git a/loader.js b/loader.js index 14b2f68d..7e757d91 100644 --- a/loader.js +++ b/loader.js @@ -3,9 +3,8 @@ const os = require('os'); const gql = require('./src'); -// Takes `source` (the source GraphQL query string) -// and `doc` (the parsed GraphQL document) and tacks on -// the imported definitions. +// Takes `source` (the source GraphQL query string) and `doc` (the parsed GraphQL document) +// and tacks on the imported definitions. function expandImports(source, doc) { const lines = source.split(/\r\n|\r|\n/); let outputCode = ` @@ -14,21 +13,17 @@ function expandImports(source, doc) { return defs.filter( function(def) { if (def.kind !== 'FragmentDefinition') return true; - var name = def.name.value - if (names[name]) { - return false; - } else { - names[name] = true; - return true; - } + var name = def.name.value; + // Filter out if seen; otherwise mark as seen and include. + return names[name] ? false : names[name] = true; } - ) + ); } `; lines.some((line) => { - if (line[0] === '#' && line.slice(1).split(' ')[0] === 'import') { - const importFile = line.slice(1).split(' ')[1]; + if (line.substr(0, 7) === '#import') { + const importFile = line.split(' ')[1]; const parseDocument = `require(${importFile})`; const appendDef = `doc.definitions = doc.definitions.concat(unique(${parseDocument}.definitions));`; outputCode += appendDef + os.EOL; @@ -47,138 +42,35 @@ module.exports = function(source) { doc.loc.source = ${JSON.stringify(doc.loc.source)}; `; - let outputCode = ""; + let outputCode = ` + module.exports = doc; + `; // Allow multiple query/mutation definitions in a file. This parses out dependencies // at compile time, and then uses those at load time to create minimal query documents // We cannot do the latter at compile time due to how the #import code works. - let operationCount = doc.definitions.reduce(function(accum, op) { - if (op.kind === "OperationDefinition") { - return accum + 1; - } + const countReducer = (accum, op) => op.kind === "OperationDefinition" ? accum + 1 : accum; + let operationCount = doc.definitions.reduce(countReducer, 0); - return accum; - }, 0); - - if (operationCount < 1) { - outputCode += ` - module.exports = doc; - ` - } else { + if (operationCount > 1) { outputCode += ` - // Collect any fragment/type references from a node, adding them to the refs Set - function collectFragmentReferences(node, refs) { - if (node.kind === "FragmentSpread") { - refs.add(node.name.value); - } else if (node.kind === "VariableDefinition") { - var type = node.type; - if (type.kind === "NamedType") { - refs.add(type.name.value); - } - } - - if (node.selectionSet) { - node.selectionSet.selections.forEach(function(selection) { - collectFragmentReferences(selection, refs); - }); - } - - if (node.variableDefinitions) { - node.variableDefinitions.forEach(function(def) { - collectFragmentReferences(def, refs); - }); - } - - if (node.definitions) { - node.definitions.forEach(function(def) { - collectFragmentReferences(def, refs); - }); - } - } - - var definitionRefs = {}; - (function extractReferences() { - doc.definitions.forEach(function(def) { - if (def.name) { - var refs = new Set(); - collectFragmentReferences(def, refs); - definitionRefs[def.name.value] = refs; - } - }); - })(); - - function findOperation(doc, name) { - for (var i = 0; i < doc.definitions.length; i++) { - var element = doc.definitions[i]; - if (element.name && element.name.value == name) { - return element; - } - } - } - - function oneQuery(doc, operationName) { - // Copy the DocumentNode, but clear out the definitions - var newDoc = { - kind: doc.kind, - definitions: [findOperation(doc, operationName)] - }; - if (doc.hasOwnProperty("loc")) { - newDoc.loc = doc.loc; - } - - // Now, for the operation we're running, find any fragments referenced by - // it or the fragments it references - var opRefs = definitionRefs[operationName] || new Set(); - var allRefs = new Set(); - var newRefs = new Set(opRefs); - while (newRefs.size > 0) { - var prevRefs = newRefs; - newRefs = new Set(); - - prevRefs.forEach(function(refName) { - if (!allRefs.has(refName)) { - allRefs.add(refName); - var childRefs = definitionRefs[refName] || new Set(); - childRefs.forEach(function(childRef) { - newRefs.add(childRef); - }); - } - }); - } - - allRefs.forEach(function(refName) { - var op = findOperation(doc, refName); - if (op) { - newDoc.definitions.push(op); - } - }); - - return newDoc; - } - - module.exports = doc; - ` - - for (const op of doc.definitions) { - if (op.kind === "OperationDefinition") { - if (!op.name) { - if (operationCount > 1) { - throw "Query/mutation names are required for a document with multiple definitions"; - } else { - continue; - } - } + var separateOperations = require('graphql/utilities/separateOperations').separateOperations; + `; + } - const opName = op.name.value; - outputCode += ` - module.exports["${opName}"] = oneQuery(doc, "${opName}"); - ` + for (const op of doc.definitions) { + const opName = op.name && op.name.value; + if (op.kind === "OperationDefinition") { + if (operationCount > 1) { + const errMsg = "Query/mutation names are required for a document with multiple definitions"; + if (!opName) throw errMsg; + outputCode += 'module.exports = separateOperations(doc);'; + } else if (opName) { + outputCode += `module.exports["${opName}"] = doc;`; } } } const importOutputCode = expandImports(source, doc); - const allCode = headerCode + os.EOL + importOutputCode + os.EOL + outputCode + os.EOL; - - return allCode; + return headerCode + os.EOL + importOutputCode + os.EOL + outputCode + os.EOL; }; diff --git a/test/graphql.js b/test/graphql.js index 726e3918..93329991 100644 --- a/test/graphql.js +++ b/test/graphql.js @@ -3,6 +3,8 @@ const gqlDefault = require('../src').default; const loader = require('../loader'); const assert = require('chai').assert; +const oldRequire = require; + [gqlRequire, gqlDefault].forEach((gql, i) => { describe(`gql ${i}`, () => { it('parses queries', () => { @@ -92,7 +94,7 @@ const assert = require('chai').assert; gql.disableExperimentalFragmentVariables() }); - + // see https://github.com/apollographql/graphql-tag/issues/168 it('does not nest queries needlessly in named exports', () => { const jsSource = loader.call({ cacheable() {} }, ` @@ -120,6 +122,7 @@ const assert = require('chai').assert; ...F2 } `); + const module = { exports: undefined }; eval(jsSource); @@ -131,17 +134,17 @@ const assert = require('chai').assert; const Q3 = module.exports.Q3.definitions; assert.equal(Q1.length, 2); - assert.equal(Q1[0].name.value, 'Q1'); - assert.equal(Q1[1].name.value, 'F1'); + assert.equal(Q1[0].name.value, 'F1'); + assert.equal(Q1[1].name.value, 'Q1'); assert.equal(Q2.length, 2); - assert.equal(Q2[0].name.value, 'Q2'); - assert.equal(Q2[1].name.value, 'F2'); + assert.equal(Q2[0].name.value, 'F2'); + assert.equal(Q2[1].name.value, 'Q2'); assert.equal(Q3.length, 3); - assert.equal(Q3[0].name.value, 'Q3'); - assert.equal(Q3[1].name.value, 'F1'); - assert.equal(Q3[2].name.value, 'F2'); + assert.equal(Q3[0].name.value, 'F1'); + assert.equal(Q3[1].name.value, 'F2'); + assert.equal(Q3[2].name.value, 'Q3'); }); @@ -176,10 +179,10 @@ const assert = require('chai').assert; const Q2 = module.exports.Q2.definitions; assert.equal(Q1.length, 4); - assert.equal(Q1[0].name.value, 'Q1'); - assert.equal(Q1[1].name.value, 'F33'); - assert.equal(Q1[2].name.value, 'F22'); - assert.equal(Q1[3].name.value, 'F11'); + assert.equal(Q1[0].name.value, 'F11'); + assert.equal(Q1[1].name.value, 'F22'); + assert.equal(Q1[2].name.value, 'F33'); + assert.equal(Q1[3].name.value, 'Q1'); assert.equal(Q2.length, 1); }); @@ -192,9 +195,8 @@ const assert = require('chai').assert; } }`; const jsSource = loader.call({ cacheable() {} }, query); - const oldRequire = require; const module = { exports: undefined }; - const require = (path) => { + let require = (path) => { assert.equal(path, './fragment_definition.graphql'); return gql` fragment authorDetails on Author { @@ -203,6 +205,7 @@ const assert = require('chai').assert; }`; }; eval(jsSource); + require = oldRequire; assert.equal(module.exports.kind, 'Document'); const definitions = module.exports.definitions; assert.equal(definitions.length, 2); @@ -225,17 +228,26 @@ const assert = require('chai').assert; } `; const jsSource = loader.call({ cacheable() {} }, query); - const oldRequire = require; const module = { exports: undefined }; - const require = (path) => { - assert.equal(path, './fragment_definition.graphql'); - return gql` + + const paths = [] + let require = (path) => { + paths.push(path); + if (path === './fragment_definition.graphql') { + return gql` fragment F222 on F { f1 f2 }`; + } else { + return oldRequire(path); + } }; + eval(jsSource); + require = oldRequire; + + assert.include(paths, './fragment_definition.graphql'); assert.exists(module.exports.Q1); assert.exists(module.exports.Q2); @@ -244,8 +256,8 @@ const assert = require('chai').assert; const Q2 = module.exports.Q2.definitions; assert.equal(Q1.length, 3); - assert.equal(Q1[0].name.value, 'Q1'); - assert.equal(Q1[1].name.value, 'F111'); + assert.equal(Q1[0].name.value, 'F111'); + assert.equal(Q1[1].name.value, 'Q1'); assert.equal(Q1[2].name.value, 'F222'); assert.equal(Q2.length, 1);