Skip to content

Commit e17ef07

Browse files
authored
feat(webpack-loader): use fork of graphql-tag/loader (#1815)
More customizable, much smaller output, bundle size improvements. Related PR: apollographql/graphql-tag#304
1 parent 71a91fe commit e17ef07

File tree

12 files changed

+460
-41
lines changed

12 files changed

+460
-41
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { normalizeString } from './utils';
2+
3+
declare global {
4+
namespace jest {
5+
interface Matchers<R, T> {
6+
/**
7+
* Normalizes whitespace and performs string comparisons
8+
*/
9+
toBeSimilarString(expected: string): R;
10+
}
11+
}
12+
}
13+
14+
expect.extend({
15+
toBeSimilarString(received: string, expected: string) {
16+
const strippedReceived = normalizeString(received);
17+
const strippedExpected = normalizeString(expected);
18+
19+
if (strippedReceived.trim() === strippedExpected.trim()) {
20+
return {
21+
message: () =>
22+
`expected
23+
${received}
24+
not to be a string containing (ignoring indents)
25+
${expected}`,
26+
pass: true,
27+
};
28+
} else {
29+
return {
30+
message: () =>
31+
`expected
32+
${received}
33+
to be a string containing (ignoring indents)
34+
${expected}`,
35+
pass: false,
36+
};
37+
}
38+
},
39+
});

packages/testing/utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import { existsSync } from 'fs';
44
import nock from 'nock';
55
import { cwd } from 'process';
66

7+
export function normalizeString(str: string) {
8+
return str.replace(/[\s,]+/g, ' ').trim();
9+
}
10+
711
type PromiseOf<T extends (...args: any[]) => any> = T extends (...args: any[]) => Promise<infer R> ? R : ReturnType<T>;
812

913
export function runTests<
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# GraphQL Tools Webpack Loader Runtime helpers
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "@graphql-tools/webpack-loader-runtime",
3+
"version": "6.0.14",
4+
"description": "A set of utils for GraphQL Webpack Loader",
5+
"repository": "[email protected]:ardatan/graphql-tools.git",
6+
"license": "MIT",
7+
"sideEffects": false,
8+
"main": "dist/index.cjs.js",
9+
"module": "dist/index.esm.js",
10+
"typings": "dist/index.d.ts",
11+
"typescript": {
12+
"definition": "dist/index.d.ts"
13+
},
14+
"peerDependencies": {
15+
"graphql": "^14.0.0 || ^15.0.0"
16+
},
17+
"buildOptions": {
18+
"input": "./src/index.ts"
19+
},
20+
"publishConfig": {
21+
"access": "public",
22+
"directory": "dist"
23+
}
24+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { DefinitionNode } from 'graphql';
2+
3+
export const uniqueCode = `
4+
const names = {};
5+
function unique(defs) {
6+
return defs.filter((def) => {
7+
if (def.kind !== 'FragmentDefinition') return true;
8+
const name = def.name.value;
9+
if (names[name]) {
10+
return false;
11+
} else {
12+
names[name] = true;
13+
return true;
14+
}
15+
});
16+
};
17+
`;
18+
19+
export function useUnique() {
20+
const names = {};
21+
return function unique(defs: DefinitionNode[]) {
22+
return defs.filter(def => {
23+
if (def.kind !== 'FragmentDefinition') return true;
24+
const name = def.name.value;
25+
if (names[name]) {
26+
return false;
27+
} else {
28+
names[name] = true;
29+
return true;
30+
}
31+
});
32+
};
33+
}

packages/webpack-loader/README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# GraphQL Tools Webpack Loader
2+
3+
A webpack loader to preprocess GraphQL Documents (operations, fragments and SDL)
4+
5+
Slightly different fork of [graphql-tag/loader](https://github.com/apollographql/graphql-tag/pull/304).
6+
7+
yarn add @graphql-tools/webpack-loader
8+
9+
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.
10+
11+
## Options
12+
13+
- noDescription (_default: false_) - removes descriptions
14+
- esModule (_default: false_) - uses import and export statements instead of CommonJS
15+
16+
## Importing GraphQL files
17+
18+
_To add support for importing `.graphql`/`.gql` files, see [Webpack loading and preprocessing](#webpack-loading-and-preprocessing) below._
19+
20+
Given a file `MyQuery.graphql`
21+
22+
```graphql
23+
query MyQuery {
24+
...
25+
}
26+
```
27+
28+
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.
29+
30+
```typescript
31+
import MyQuery from './query.graphql'
32+
```
33+
34+
### Preprocessing queries and fragments
35+
36+
Preprocessing GraphQL queries and fragments into ASTs at build time can greatly improve load times.
37+
38+
#### Webpack loading and preprocessing
39+
40+
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.
41+
42+
```js
43+
{
44+
loaders: [
45+
{
46+
test: /\.(graphql|gql)$/,
47+
exclude: /node_modules/,
48+
loader: '@graphql-tools/webpack-loader',
49+
options: {
50+
/* ... */
51+
}
52+
}
53+
],
54+
}
55+
```

packages/webpack-loader/package.json

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,11 @@
1818
"input": "./src/index.ts"
1919
},
2020
"dependencies": {
21-
"@graphql-tools/load": "6.0.14",
22-
"@graphql-tools/graphql-file-loader": "6.0.14",
23-
"loader-utils": "2.0.0",
21+
"@graphql-tools/webpack-loader-runtime": "6.0.14",
2422
"tslib": "~2.0.0"
2523
},
26-
"devDependencies": {
27-
"@types/loader-utils": "2.0.1"
28-
},
2924
"publishConfig": {
3025
"access": "public",
3126
"directory": "dist"
3227
}
33-
}
28+
}
Lines changed: 108 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,114 @@
1-
import { loadTypedefs } from '@graphql-tools/load';
2-
import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader';
3-
import { concatAST } from 'graphql';
4-
import { getOptions } from 'loader-utils';
1+
import os from 'os';
2+
import { isExecutableDefinitionNode, visit, Kind, DocumentNode } from 'graphql';
3+
import { uniqueCode } from '@graphql-tools/webpack-loader-runtime';
4+
import { parseDocument } from './parser';
55

6-
export default function (this: any, path: string) {
7-
const callback = this.async();
6+
function isSDL(doc: DocumentNode) {
7+
return !doc.definitions.some(def => isExecutableDefinitionNode(def));
8+
}
89

9-
this.cacheable();
10+
function removeDescriptions(doc: DocumentNode) {
11+
function transformNode(node: any) {
12+
if (node.description) {
13+
node.description = undefined;
14+
}
15+
16+
return node;
17+
}
18+
19+
if (isSDL(doc)) {
20+
return visit(doc, {
21+
ScalarTypeDefinition: transformNode,
22+
ObjectTypeDefinition: transformNode,
23+
InterfaceTypeDefinition: transformNode,
24+
UnionTypeDefinition: transformNode,
25+
EnumTypeDefinition: transformNode,
26+
EnumValueDefinition: transformNode,
27+
InputObjectTypeDefinition: transformNode,
28+
InputValueDefinition: transformNode,
29+
FieldDefinition: transformNode,
30+
});
31+
}
32+
33+
return doc;
34+
}
1035

11-
const options = getOptions(this);
36+
interface Options {
37+
noDescription?: boolean;
38+
esModule?: boolean;
39+
importHelpers?: boolean;
40+
}
41+
42+
function expandImports(source: string, options: Options) {
43+
const lines = source.split(/\r\n|\r|\n/);
44+
let outputCode = options.importHelpers
45+
? `
46+
const { useUnique } = require('@graphql-tools/webpack-loader-runtime');
47+
48+
const unique = useUnique();
49+
`
50+
: `
51+
${uniqueCode}
52+
`;
1253

13-
loadTypedefs(path, {
14-
loaders: [new GraphQLFileLoader()],
15-
noLocation: true,
16-
...options,
17-
}).then(sources => {
18-
const documents = sources.map(source => source.document);
19-
const mergedDoc = concatAST(documents);
20-
return callback(null, `export default ${JSON.stringify(mergedDoc)}`);
54+
lines.some(line => {
55+
if (line[0] === '#' && line.slice(1).split(' ')[0] === 'import') {
56+
const importFile = line.slice(1).split(' ')[1];
57+
const parseDocument = `require(${importFile})`;
58+
const appendDef = `doc.definitions = doc.definitions.concat(unique(${parseDocument}.definitions));`;
59+
outputCode += appendDef + os.EOL;
60+
}
61+
return line.length !== 0 && line[0] !== '#';
2162
});
63+
64+
return outputCode;
65+
}
66+
67+
export default function graphqlLoader(source: string) {
68+
this.cacheable();
69+
const options: Options = this.query || {};
70+
let doc = parseDocument(source);
71+
72+
// Removes descriptions from Nodes
73+
if (options.noDescription) {
74+
doc = removeDescriptions(doc);
75+
}
76+
77+
const headerCode = `
78+
const doc = ${JSON.stringify(doc)};
79+
`;
80+
81+
let outputCode = '';
82+
83+
// Allow multiple query/mutation definitions in a file. This parses out dependencies
84+
// at compile time, and then uses those at load time to create minimal query documents
85+
// We cannot do the latter at compile time due to how the #import code works.
86+
const operationCount = doc.definitions.reduce<number>((accum, op) => {
87+
if (op.kind === Kind.OPERATION_DEFINITION) {
88+
return accum + 1;
89+
}
90+
91+
return accum;
92+
}, 0);
93+
94+
function exportDefaultStatement(identifier: string) {
95+
if (options.esModule) {
96+
return `export default ${identifier}`;
97+
}
98+
99+
return `module.exports = ${identifier}`;
100+
}
101+
102+
if (operationCount > 1) {
103+
throw new Error('GraphQL Webpack Loader allows only for one GraphQL Operation per file');
104+
}
105+
106+
outputCode += `
107+
${exportDefaultStatement('doc')}
108+
`;
109+
110+
const importOutputCode = expandImports(source, options);
111+
const allCode = [headerCode, importOutputCode, outputCode, ''].join(os.EOL);
112+
113+
return allCode;
22114
}

0 commit comments

Comments
 (0)