-
-
Notifications
You must be signed in to change notification settings - Fork 820
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(webpack-loader): use fork of graphql-tag/loader (#1815)
More customizable, much smaller output, bundle size improvements. Related PR: apollographql/graphql-tag#304
- Loading branch information
1 parent
71a91fe
commit e17ef07
Showing
12 changed files
with
460 additions
and
41 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { normalizeString } from './utils'; | ||
|
||
declare global { | ||
namespace jest { | ||
interface Matchers<R, T> { | ||
/** | ||
* Normalizes whitespace and performs string comparisons | ||
*/ | ||
toBeSimilarString(expected: string): R; | ||
} | ||
} | ||
} | ||
|
||
expect.extend({ | ||
toBeSimilarString(received: string, expected: string) { | ||
const strippedReceived = normalizeString(received); | ||
const strippedExpected = normalizeString(expected); | ||
|
||
if (strippedReceived.trim() === strippedExpected.trim()) { | ||
return { | ||
message: () => | ||
`expected | ||
${received} | ||
not to be a string containing (ignoring indents) | ||
${expected}`, | ||
pass: true, | ||
}; | ||
} else { | ||
return { | ||
message: () => | ||
`expected | ||
${received} | ||
to be a string containing (ignoring indents) | ||
${expected}`, | ||
pass: false, | ||
}; | ||
} | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# GraphQL Tools Webpack Loader Runtime helpers |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
{ | ||
"name": "@graphql-tools/webpack-loader-runtime", | ||
"version": "6.0.14", | ||
"description": "A set of utils for GraphQL Webpack Loader", | ||
"repository": "[email protected]:ardatan/graphql-tools.git", | ||
"license": "MIT", | ||
"sideEffects": false, | ||
"main": "dist/index.cjs.js", | ||
"module": "dist/index.esm.js", | ||
"typings": "dist/index.d.ts", | ||
"typescript": { | ||
"definition": "dist/index.d.ts" | ||
}, | ||
"peerDependencies": { | ||
"graphql": "^14.0.0 || ^15.0.0" | ||
}, | ||
"buildOptions": { | ||
"input": "./src/index.ts" | ||
}, | ||
"publishConfig": { | ||
"access": "public", | ||
"directory": "dist" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { DefinitionNode } from 'graphql'; | ||
|
||
export const uniqueCode = ` | ||
const names = {}; | ||
function unique(defs) { | ||
return defs.filter((def) => { | ||
if (def.kind !== 'FragmentDefinition') return true; | ||
const name = def.name.value; | ||
if (names[name]) { | ||
return false; | ||
} else { | ||
names[name] = true; | ||
return true; | ||
} | ||
}); | ||
}; | ||
`; | ||
|
||
export function useUnique() { | ||
const names = {}; | ||
return function unique(defs: DefinitionNode[]) { | ||
return defs.filter(def => { | ||
if (def.kind !== 'FragmentDefinition') return true; | ||
const name = def.name.value; | ||
if (names[name]) { | ||
return false; | ||
} else { | ||
names[name] = true; | ||
return true; | ||
} | ||
}); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
# GraphQL Tools Webpack Loader | ||
|
||
A webpack loader to preprocess GraphQL Documents (operations, fragments and SDL) | ||
|
||
Slightly different fork of [graphql-tag/loader](https://github.com/apollographql/graphql-tag/pull/304). | ||
|
||
yarn add @graphql-tools/webpack-loader | ||
|
||
How is it different from `graphql-tag`? It removes locations entirely, doesn't include sources (string content of imported files), no warnings about duplicated fragment names and supports more custom scenarios. | ||
|
||
## Options | ||
|
||
- noDescription (_default: false_) - removes descriptions | ||
- esModule (_default: false_) - uses import and export statements instead of CommonJS | ||
|
||
## Importing GraphQL files | ||
|
||
_To add support for importing `.graphql`/`.gql` files, see [Webpack loading and preprocessing](#webpack-loading-and-preprocessing) below._ | ||
|
||
Given a file `MyQuery.graphql` | ||
|
||
```graphql | ||
query MyQuery { | ||
... | ||
} | ||
``` | ||
|
||
If you have configured [the webpack @graphql-tools/webpack-loader](#webpack-loading-and-preprocessing), you can import modules containing graphQL queries. The imported value will be the pre-built AST. | ||
|
||
```typescript | ||
import MyQuery from './query.graphql' | ||
``` | ||
|
||
### Preprocessing queries and fragments | ||
|
||
Preprocessing GraphQL queries and fragments into ASTs at build time can greatly improve load times. | ||
|
||
#### Webpack loading and preprocessing | ||
|
||
Using the included `@graphql-tools/webpack-loader` it is possible to maintain query logic that is separate from the rest of your application logic. With the loader configured, imported graphQL files will be converted to AST during the webpack build process. | ||
|
||
```js | ||
{ | ||
loaders: [ | ||
{ | ||
test: /\.(graphql|gql)$/, | ||
exclude: /node_modules/, | ||
loader: '@graphql-tools/webpack-loader', | ||
options: { | ||
/* ... */ | ||
} | ||
} | ||
], | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,114 @@ | ||
import { loadTypedefs } from '@graphql-tools/load'; | ||
import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader'; | ||
import { concatAST } from 'graphql'; | ||
import { getOptions } from 'loader-utils'; | ||
import os from 'os'; | ||
import { isExecutableDefinitionNode, visit, Kind, DocumentNode } from 'graphql'; | ||
import { uniqueCode } from '@graphql-tools/webpack-loader-runtime'; | ||
import { parseDocument } from './parser'; | ||
|
||
export default function (this: any, path: string) { | ||
const callback = this.async(); | ||
function isSDL(doc: DocumentNode) { | ||
return !doc.definitions.some(def => isExecutableDefinitionNode(def)); | ||
} | ||
|
||
this.cacheable(); | ||
function removeDescriptions(doc: DocumentNode) { | ||
function transformNode(node: any) { | ||
if (node.description) { | ||
node.description = undefined; | ||
} | ||
|
||
return node; | ||
} | ||
|
||
if (isSDL(doc)) { | ||
return visit(doc, { | ||
ScalarTypeDefinition: transformNode, | ||
ObjectTypeDefinition: transformNode, | ||
InterfaceTypeDefinition: transformNode, | ||
UnionTypeDefinition: transformNode, | ||
EnumTypeDefinition: transformNode, | ||
EnumValueDefinition: transformNode, | ||
InputObjectTypeDefinition: transformNode, | ||
InputValueDefinition: transformNode, | ||
FieldDefinition: transformNode, | ||
}); | ||
} | ||
|
||
return doc; | ||
} | ||
|
||
const options = getOptions(this); | ||
interface Options { | ||
noDescription?: boolean; | ||
esModule?: boolean; | ||
importHelpers?: boolean; | ||
} | ||
|
||
function expandImports(source: string, options: Options) { | ||
const lines = source.split(/\r\n|\r|\n/); | ||
let outputCode = options.importHelpers | ||
? ` | ||
const { useUnique } = require('@graphql-tools/webpack-loader-runtime'); | ||
const unique = useUnique(); | ||
` | ||
: ` | ||
${uniqueCode} | ||
`; | ||
|
||
loadTypedefs(path, { | ||
loaders: [new GraphQLFileLoader()], | ||
noLocation: true, | ||
...options, | ||
}).then(sources => { | ||
const documents = sources.map(source => source.document); | ||
const mergedDoc = concatAST(documents); | ||
return callback(null, `export default ${JSON.stringify(mergedDoc)}`); | ||
lines.some(line => { | ||
if (line[0] === '#' && line.slice(1).split(' ')[0] === 'import') { | ||
const importFile = line.slice(1).split(' ')[1]; | ||
const parseDocument = `require(${importFile})`; | ||
const appendDef = `doc.definitions = doc.definitions.concat(unique(${parseDocument}.definitions));`; | ||
outputCode += appendDef + os.EOL; | ||
} | ||
return line.length !== 0 && line[0] !== '#'; | ||
}); | ||
|
||
return outputCode; | ||
} | ||
|
||
export default function graphqlLoader(source: string) { | ||
this.cacheable(); | ||
const options: Options = this.query || {}; | ||
let doc = parseDocument(source); | ||
|
||
// Removes descriptions from Nodes | ||
if (options.noDescription) { | ||
doc = removeDescriptions(doc); | ||
} | ||
|
||
const headerCode = ` | ||
const doc = ${JSON.stringify(doc)}; | ||
`; | ||
|
||
let outputCode = ''; | ||
|
||
// 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. | ||
const operationCount = doc.definitions.reduce<number>((accum, op) => { | ||
if (op.kind === Kind.OPERATION_DEFINITION) { | ||
return accum + 1; | ||
} | ||
|
||
return accum; | ||
}, 0); | ||
|
||
function exportDefaultStatement(identifier: string) { | ||
if (options.esModule) { | ||
return `export default ${identifier}`; | ||
} | ||
|
||
return `module.exports = ${identifier}`; | ||
} | ||
|
||
if (operationCount > 1) { | ||
throw new Error('GraphQL Webpack Loader allows only for one GraphQL Operation per file'); | ||
} | ||
|
||
outputCode += ` | ||
${exportDefaultStatement('doc')} | ||
`; | ||
|
||
const importOutputCode = expandImports(source, options); | ||
const allCode = [headerCode, importOutputCode, outputCode, ''].join(os.EOL); | ||
|
||
return allCode; | ||
} |
Oops, something went wrong.