diff --git a/config/helpers/evaluate_module_from_source.js b/config/helpers/evaluate_module_from_source.js new file mode 100644 index 0000000000000000000000000000000000000000..b55bbb82b63c0a415c8631da37d4f22dfb2b7d96 --- /dev/null +++ b/config/helpers/evaluate_module_from_source.js @@ -0,0 +1,37 @@ +const vm = require('vm'); + +/** + * This function uses Node's `vm` modules to evaluate the `module.exports` of a given source string + * + * Example: + * + * ```javascript + * const { exports: moduleExports } = evaluateModuleFromSource("const foo = 7;\n module.exports.bar = 10 + foo;"); + * + * assert(moduleExports.bar === 17); + * ``` + * + * @param {String} source to be evaluated using Node's `vm` modules + * @param {{ require: Function }} options used in the context during evaluation of the Node module + * @returns {{ exports: any }} exports added to the script's `module.exports` context + */ +const evaluateModuleFromSource = (source, { require } = {}) => { + const context = { + module: { + exports: {}, + }, + require, + }; + + try { + const script = new vm.Script(source); + script.runInNewContext(context); + } catch (e) { + console.error(e); + throw e; + } + + return context.module; +}; + +module.exports = { evaluateModuleFromSource }; diff --git a/config/plugins/graphql_known_operations_plugin.js b/config/plugins/graphql_known_operations_plugin.js index 164b34c1dd13ee2deebf8f79c2ccacd37d27a716..c340849e084ce4e35e3ae6e503cae948a3277898 100644 --- a/config/plugins/graphql_known_operations_plugin.js +++ b/config/plugins/graphql_known_operations_plugin.js @@ -1,9 +1,10 @@ /* 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$/; -const OPERATION_NAME_SOURCE_REGEX = /^\s*module\.exports.*oneQuery.*"(\w+)"/gm; /** * Returns whether a given webpack module is a "graphql" module @@ -26,9 +27,19 @@ const getOperationNames = (module) => { return []; } - const matches = originalSource.source().toString().matchAll(OPERATION_NAME_SOURCE_REGEX); + 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 Array.from(matches).map((match) => match[1]); + return names; }; const createFileContents = (knownOperations) => { @@ -60,7 +71,7 @@ const onSucceedModule = ({ module, knownOperations }) => { return; } - getOperationNames(module).forEach((x) => knownOperations.add(x)); + getOperationNames(module).forEach((name) => knownOperations.add(name)); }; const onCompilerEmit = ({ compilation, knownOperations, filename }) => {