Skip to content

Commit

Permalink
feat(webpack-loader): use fork of graphql-tag/loader (#1815)
Browse files Browse the repository at this point in the history
More customizable, much smaller output, bundle size improvements.

Related PR: apollographql/graphql-tag#304
  • Loading branch information
kamilkisiela committed Jul 22, 2020
1 parent 71a91fe commit e17ef07
Show file tree
Hide file tree
Showing 12 changed files with 460 additions and 41 deletions.
39 changes: 39 additions & 0 deletions packages/testing/to-be-similar-string.ts
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,
};
}
},
});
4 changes: 4 additions & 0 deletions packages/testing/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { existsSync } from 'fs';
import nock from 'nock';
import { cwd } from 'process';

export function normalizeString(str: string) {
return str.replace(/[\s,]+/g, ' ').trim();
}

type PromiseOf<T extends (...args: any[]) => any> = T extends (...args: any[]) => Promise<infer R> ? R : ReturnType<T>;

export function runTests<
Expand Down
1 change: 1 addition & 0 deletions packages/webpack-loader-runtime/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# GraphQL Tools Webpack Loader Runtime helpers
24 changes: 24 additions & 0 deletions packages/webpack-loader-runtime/package.json
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": "git@github.com: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"
}
}
33 changes: 33 additions & 0 deletions packages/webpack-loader-runtime/src/index.ts
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;
}
});
};
}
55 changes: 55 additions & 0 deletions packages/webpack-loader/README.md
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: {
/* ... */
}
}
],
}
```
9 changes: 2 additions & 7 deletions packages/webpack-loader/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,11 @@
"input": "./src/index.ts"
},
"dependencies": {
"@graphql-tools/load": "6.0.14",
"@graphql-tools/graphql-file-loader": "6.0.14",
"loader-utils": "2.0.0",
"@graphql-tools/webpack-loader-runtime": "6.0.14",
"tslib": "~2.0.0"
},
"devDependencies": {
"@types/loader-utils": "2.0.1"
},
"publishConfig": {
"access": "public",
"directory": "dist"
}
}
}
124 changes: 108 additions & 16 deletions packages/webpack-loader/src/index.ts
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;
}

0 comments on commit e17ef07

Please sign in to comment.