summaryrefslogtreecommitdiff
path: root/config/plugins/graphql_known_operations_plugin.js
blob: c340849e084ce4e35e3ae6e503cae948a3277898 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
/* eslint-disable no-underscore-dangle */
const yaml = require('js-yaml');

const { evaluateModuleFromSource } = require('../helpers/evaluate_module_from_source');

const PLUGIN_NAME = 'GraphqlKnownOperationsPlugin';
const GRAPHQL_PATH_REGEX = /(query|mutation)\.graphql$/;

/**
 * Returns whether a given webpack module is a "graphql" module
 */
const isGraphqlModule = (module) => {
  return GRAPHQL_PATH_REGEX.test(module.resource);
};

/**
 * Returns graphql operation names we can parse from the given module
 *
 * Since webpack gives us the source **after** the graphql-tag/loader runs,
 * we can look for specific lines we're guaranteed to have from the
 * graphql-tag/loader.
 */
const getOperationNames = (module) => {
  const originalSource = module.originalSource();

  if (!originalSource) {
    return [];
  }

  const { exports: moduleExports } = evaluateModuleFromSource(originalSource.source().toString(), {
    // what: stub require(...) when evaluating the graphql module
    // why: require(...) is used to fetch fragments. We only need operation metadata, so it's fine to stub these out.
    require: () => ({ definitions: [] }),
  });

  const names = moduleExports.definitions
    .filter((x) => ['query', 'mutation'].includes(x.operation))
    .map((x) => x.name?.value)
    // why: It's possible for operations to not have a name. That violates our eslint rule, but either way, let's ignore those here.
    .filter(Boolean);

  return names;
};

const createFileContents = (knownOperations) => {
  const sourceData = Array.from(knownOperations.values()).sort((a, b) => a.localeCompare(b));

  return yaml.dump(sourceData);
};

/**
 * Creates a webpack4 compatible "RawSource"
 *
 * Inspired from https://sourcegraph.com/github.com/FormidableLabs/webpack-stats-plugin@e050ff8c362d5ddd45c66ade724d4a397ace3e5c/-/blob/lib/stats-writer-plugin.js?L144
 */
const createWebpackRawSource = (source) => {
  const buff = Buffer.from(source, 'utf-8');

  return {
    source() {
      return buff;
    },
    size() {
      return buff.length;
    },
  };
};

const onSucceedModule = ({ module, knownOperations }) => {
  if (!isGraphqlModule(module)) {
    return;
  }

  getOperationNames(module).forEach((name) => knownOperations.add(name));
};

const onCompilerEmit = ({ compilation, knownOperations, filename }) => {
  const contents = createFileContents(knownOperations);
  const source = createWebpackRawSource(contents);

  const asset = compilation.getAsset(filename);
  if (asset) {
    compilation.updateAsset(filename, source);
  } else {
    compilation.emitAsset(filename, source);
  }
};

/**
 * Webpack plugin that outputs a file containing known graphql operations.
 *
 * A lot of the mechanices was expired from [this example][1].
 *
 * [1]: https://sourcegraph.com/github.com/FormidableLabs/webpack-stats-plugin@e050ff8c362d5ddd45c66ade724d4a397ace3e5c/-/blob/lib/stats-writer-plugin.js?L136
 */
class GraphqlKnownOperationsPlugin {
  constructor({ filename }) {
    this._filename = filename;
  }

  apply(compiler) {
    const knownOperations = new Set();

    compiler.hooks.emit.tap(PLUGIN_NAME, (compilation) => {
      onCompilerEmit({
        compilation,
        knownOperations,
        filename: this._filename,
      });
    });

    compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
      compilation.hooks.succeedModule.tap(PLUGIN_NAME, (module) => {
        onSucceedModule({
          module,
          knownOperations,
        });
      });
    });
  }
}

module.exports = GraphqlKnownOperationsPlugin;