@graphql-tools/webpack-loader
Version:
A set of utils for faster development of GraphQL tools
383 lines (369 loc) • 13.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = graphqlLoader;
const tslib_1 = require("tslib");
const os_1 = tslib_1.__importDefault(require("os"));
const path_1 = tslib_1.__importDefault(require("path"));
const graphql_1 = require("graphql");
const optimize_1 = require("@graphql-tools/optimize");
const webpack_loader_runtime_1 = require("@graphql-tools/webpack-loader-runtime");
const parser_js_1 = require("./parser.js");
function isNamedExecutableDefinition(def) {
return ((def.kind === graphql_1.Kind.OPERATION_DEFINITION || def.kind === graphql_1.Kind.FRAGMENT_DEFINITION) &&
def.name != null);
}
const DOCUMENT_EXPORT_KEYS = new Set(['kind', 'definitions', 'loc']);
function isGraphQLName(name) {
return /^[_A-Za-z][_0-9A-Za-z]*$/.test(name);
}
// $ prefixes cannot appear in GraphQL names, so these bindings cannot collide
// with operation or fragment exports.
const oneQueryCode = `
function $gqlCollectFragmentReferences(node, refs) {
if (node.kind === "FragmentSpread") {
refs.add(node.name.value);
}
if (node.selectionSet) {
node.selectionSet.selections.forEach(function (selection) {
$gqlCollectFragmentReferences(selection, refs);
});
}
if (node.variableDefinitions) {
node.variableDefinitions.forEach(function (def) {
$gqlCollectFragmentReferences(def, refs);
});
}
if (node.definitions) {
node.definitions.forEach(function (def) {
$gqlCollectFragmentReferences(def, refs);
});
}
}
var $gqlDefinitionRefs = Object.create(null);
(function $gqlExtractReferences() {
doc.definitions.forEach(function (def) {
if (def.name) {
var refs = new Set();
$gqlCollectFragmentReferences(def, refs);
$gqlDefinitionRefs[def.name.value] = refs;
}
});
})();
function $gqlFindDefinition(doc, name) {
for (var i = 0; i < doc.definitions.length; i++) {
var element = doc.definitions[i];
if (element.name && element.name.value == name) {
return element;
}
}
}
function $gqlOneQuery(doc, operationName) {
var newDoc = {
kind: doc.kind,
definitions: [$gqlFindDefinition(doc, operationName)]
};
if (doc.hasOwnProperty("loc")) {
newDoc.loc = doc.loc;
}
var opRefs = $gqlDefinitionRefs[operationName] || new Set();
var allRefs = new Set();
var newRefs = new Set();
opRefs.forEach(function (refName) {
newRefs.add(refName);
});
while (newRefs.size > 0) {
var prevRefs = newRefs;
newRefs = new Set();
prevRefs.forEach(function (refName) {
if (!allRefs.has(refName)) {
allRefs.add(refName);
var childRefs = $gqlDefinitionRefs[refName] || new Set();
childRefs.forEach(function (childRef) {
newRefs.add(childRef);
});
}
});
}
allRefs.forEach(function (refName) {
var op = $gqlFindDefinition(doc, refName);
if (op) {
newDoc.definitions.push(op);
}
});
return newDoc;
}
function $gqlEachNamedDefinition(callback) {
var seen = Object.create(null);
doc.definitions.forEach(function (def) {
if (
(def.kind !== "OperationDefinition" && def.kind !== "FragmentDefinition") ||
!def.name
) {
return;
}
var opName = def.name.value;
if (seen[opName] && seen[opName] !== def.kind) {
throw new Error(
'GraphQL operation and fragment both named "' +
opName +
'" cannot be exported from the same file'
);
}
seen[opName] = def.kind;
callback(opName);
});
}
`;
function isSDL(doc) {
return !doc.definitions.some(def => (0, graphql_1.isExecutableDefinitionNode)(def));
}
function expandImports(source, options) {
const lines = source.split(/\r\n|\r|\n/);
let outputCode = options.importHelpers
? `
var useUnique = require('@graphql-tools/webpack-loader-runtime').useUnique;
var unique = useUnique();
`
: `
${webpack_loader_runtime_1.uniqueCode}
`;
lines.some(line => {
if (line[0] === '#' && line.slice(1).split(' ')[0] === 'import') {
const importFile = line.slice(1).split(' ')[1];
const parseDocument = options.esModule
? `require(${importFile}).default`
: `require(${importFile})`;
const appendDef = `doc.definitions = doc.definitions.concat(unique(${parseDocument}.definitions));`;
outputCode += appendDef + os_1.default.EOL;
}
return line.length !== 0 && line[0] !== '#';
});
return outputCode;
}
function parseImportPaths(source) {
const importPaths = [];
const lines = source.split(/\r\n|\r|\n/);
for (const line of lines) {
if (line[0] === '#' && line.slice(1).split(' ')[0] === 'import') {
const raw = line.slice(1).split(' ')[1];
if (raw) {
try {
importPaths.push(JSON.parse(raw));
}
catch {
importPaths.push(raw.replace(/^['"]|['"]$/g, ''));
}
}
}
else if (line.length !== 0 && line[0] !== '#') {
break;
}
}
return importPaths;
}
function loaderContextDir(loader) {
if (loader.context) {
return loader.context;
}
if (loader.resourcePath) {
return path_1.default.dirname(loader.resourcePath);
}
return '';
}
function resolveImportWithWebpack(loader, contextDir, importPath) {
return new Promise(resolve => {
loader.resolve(contextDir, importPath, (error, result) => {
if (error || result == null) {
resolve(undefined);
return;
}
resolve(String(result));
});
});
}
function resolveImportPath(contextDir, importPath) {
if (!contextDir) {
return undefined;
}
return path_1.default.resolve(contextDir, importPath);
}
function readResolvedSource(loader, resolved, seenFiles) {
const fs = loader.fs;
if (fs?.readFileSync == null) {
return undefined;
}
if (seenFiles.has(resolved)) {
return undefined;
}
seenFiles.add(resolved);
try {
loader.addDependency(resolved);
const data = fs.readFileSync(resolved);
return typeof data === 'string' ? data : data.toString('utf8');
}
catch {
return undefined;
}
}
async function collectImportedNamedDefinitions(source, loader, contextDir, seenFiles) {
const collected = [];
for (const importPath of parseImportPaths(source)) {
const resolved = await resolveImportWithWebpack(loader, contextDir, importPath);
if (resolved == null) {
continue;
}
const imported = readResolvedSource(loader, resolved, seenFiles);
if (imported == null) {
continue;
}
const importedDoc = (0, parser_js_1.parseDocument)(imported);
collected.push(...importedDoc.definitions.filter(isNamedExecutableDefinition));
collected.push(...(await collectImportedNamedDefinitions(imported, loader, path_1.default.dirname(resolved), seenFiles)));
}
return collected;
}
function collectImportedNamedDefinitionsSync(source, loader, contextDir, seenFiles) {
const collected = [];
for (const importPath of parseImportPaths(source)) {
const resolved = resolveImportPath(contextDir, importPath);
if (resolved == null) {
continue;
}
const imported = readResolvedSource(loader, resolved, seenFiles);
if (imported == null) {
continue;
}
const importedDoc = (0, parser_js_1.parseDocument)(imported);
collected.push(...importedDoc.definitions.filter(isNamedExecutableDefinition));
collected.push(...collectImportedNamedDefinitionsSync(imported, loader, path_1.default.dirname(resolved), seenFiles));
}
return collected;
}
function emitLoaderCode(source, options, importedNamedDefinitions) {
let doc = (0, parser_js_1.parseDocument)(source);
const optimizers = [];
if (options.noDescription) {
optimizers.push(optimize_1.removeDescriptions);
}
if (options.noEmptyNodes) {
optimizers.push(optimize_1.removeEmptyNodes);
}
if (optimizers.length > 0 && isSDL(doc)) {
doc = (0, optimize_1.optimizeDocumentNode)(doc, optimizers);
}
let stringifiedDoc = JSON.stringify(doc);
if (options.replaceKinds) {
for (const identifier in graphql_1.Kind) {
const value = graphql_1.Kind[identifier];
stringifiedDoc = stringifiedDoc.replace(new RegExp(`"kind":"${value}"`, 'g'), `"kind": Kind.${identifier}`);
}
}
const headerCode = `
${options.replaceKinds ? "var Kind = require('graphql/language/kinds');" : ''}
var doc = ${stringifiedDoc};
`;
let outputCode = '';
// Named exports are built at load time so #import can append fragments first.
const operations = doc.definitions.filter((def) => def.kind === graphql_1.Kind.OPERATION_DEFINITION);
const namedDefinitions = [
...doc.definitions.filter(isNamedExecutableDefinition),
...importedNamedDefinitions,
];
if (operations.length > 1 && operations.some(op => op.name == null)) {
throw new Error('GraphQL operation names are required when a file contains multiple operations');
}
function exportDefaultStatement(identifier) {
if (options.esModule) {
return `export default ${identifier}`;
}
return `module.exports = ${identifier}`;
}
if (namedDefinitions.length === 0 || options.esModule) {
outputCode += `
${exportDefaultStatement('doc')}
`;
}
const hasImports = parseImportPaths(source).length > 0;
if (namedDefinitions.length > 0 || (!options.esModule && hasImports)) {
const seenNames = new Map();
for (const def of namedDefinitions) {
const opName = def.name.value;
const previousKind = seenNames.get(opName);
if (previousKind && previousKind !== def.kind) {
throw new Error(`GraphQL operation and fragment both named "${opName}" cannot be exported from the same file`);
}
seenNames.set(opName, def.kind);
if (!isGraphQLName(opName)) {
throw new Error(`Cannot export GraphQL operation or fragment named ${JSON.stringify(opName)}`);
}
if (options.esModule && opName === 'default') {
throw new Error('Cannot export a GraphQL operation or fragment named "default" because the loader already emits a default export');
}
if (!options.esModule && DOCUMENT_EXPORT_KEYS.has(opName)) {
throw new Error(`Cannot export GraphQL operation or fragment named "${opName}" because it would replace the default document`);
}
}
outputCode += oneQueryCode;
if (!options.esModule) {
outputCode += `
var $gqlExports = { kind: doc.kind, definitions: doc.definitions };
if (doc.hasOwnProperty("loc")) {
$gqlExports.loc = doc.loc;
}
module.exports = $gqlExports;
$gqlEachNamedDefinition(function (opName) {
if (opName === "kind" || opName === "definitions" || opName === "loc") {
throw new Error(
'Cannot export GraphQL operation or fragment named "' +
opName +
'" because it would replace the default document'
);
}
Object.defineProperty($gqlExports, opName, {
value: $gqlOneQuery(doc, opName),
enumerable: true,
configurable: true,
writable: true
});
});
`;
}
if (options.esModule) {
const emitted = new Set();
namedDefinitions.forEach((def, index) => {
const opName = def.name.value;
if (emitted.has(opName)) {
return;
}
emitted.add(opName);
outputCode += `
var $gql_exp_${index} = $gqlOneQuery(doc, ${JSON.stringify(opName)});
export { $gql_exp_${index} as ${opName} };
`;
});
}
}
const importOutputCode = expandImports(source, options);
const allCode = [headerCode, importOutputCode, outputCode, ''].join(os_1.default.EOL);
return allCode;
}
function graphqlLoader(source) {
this.cacheable();
// TODO: This should probably use this.getOptions()
const options = this.query || {};
const contextDir = loaderContextDir(this);
const needsImportedNames = parseImportPaths(source).length > 0;
if (!needsImportedNames) {
return emitLoaderCode(source, options, []);
}
if (typeof this.resolve === 'function') {
const callback = typeof this.async === 'function' ? this.async() : undefined;
const pending = collectImportedNamedDefinitions(source, this, contextDir, new Set()).then(imported => emitLoaderCode(source, options, imported));
if (callback) {
pending.then(code => callback(null, code), error => callback(error));
return;
}
return pending;
}
return emitLoaderCode(source, options, collectImportedNamedDefinitionsSync(source, this, contextDir, new Set()));
}