UNPKG

@graphql-codegen/visitor-plugin-common

Version:
361 lines (360 loc) • 18.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ClientSideBaseVisitor = exports.DocumentMode = void 0; const tslib_1 = require("tslib"); const path_1 = require("path"); const plugin_helpers_1 = require("@graphql-codegen/plugin-helpers"); const optimize_1 = require("@graphql-tools/optimize"); const auto_bind_1 = tslib_1.__importDefault(require("auto-bind")); const change_case_all_1 = require("change-case-all"); const dependency_graph_1 = require("dependency-graph"); const graphql_1 = require("graphql"); const graphql_tag_1 = tslib_1.__importDefault(require("graphql-tag")); const base_visitor_js_1 = require("./base-visitor.js"); const imports_js_1 = require("./imports.js"); const utils_js_1 = require("./utils.js"); graphql_tag_1.default.enableExperimentalFragmentVariables(); var DocumentMode; (function (DocumentMode) { DocumentMode["graphQLTag"] = "graphQLTag"; DocumentMode["documentNode"] = "documentNode"; DocumentMode["documentNodeImportFragments"] = "documentNodeImportFragments"; DocumentMode["external"] = "external"; DocumentMode["string"] = "string"; })(DocumentMode = exports.DocumentMode || (exports.DocumentMode = {})); const EXTENSIONS_TO_REMOVE = ['.ts', '.tsx', '.js', '.jsx']; class ClientSideBaseVisitor extends base_visitor_js_1.BaseVisitor { constructor(_schema, _fragments, rawConfig, additionalConfig, documents) { super(rawConfig, { scalars: (0, utils_js_1.buildScalarsFromConfig)(_schema, rawConfig), dedupeOperationSuffix: (0, utils_js_1.getConfigValue)(rawConfig.dedupeOperationSuffix, false), optimizeDocumentNode: (0, utils_js_1.getConfigValue)(rawConfig.optimizeDocumentNode, true), omitOperationSuffix: (0, utils_js_1.getConfigValue)(rawConfig.omitOperationSuffix, false), gqlImport: rawConfig.gqlImport || null, documentNodeImport: rawConfig.documentNodeImport || null, noExport: Boolean(rawConfig.noExport), importOperationTypesFrom: (0, utils_js_1.getConfigValue)(rawConfig.importOperationTypesFrom, null), operationResultSuffix: (0, utils_js_1.getConfigValue)(rawConfig.operationResultSuffix, ''), documentVariablePrefix: (0, utils_js_1.getConfigValue)(rawConfig.documentVariablePrefix, ''), documentVariableSuffix: (0, utils_js_1.getConfigValue)(rawConfig.documentVariableSuffix, 'Document'), fragmentVariablePrefix: (0, utils_js_1.getConfigValue)(rawConfig.fragmentVariablePrefix, ''), fragmentVariableSuffix: (0, utils_js_1.getConfigValue)(rawConfig.fragmentVariableSuffix, 'FragmentDoc'), documentMode: ((rawConfig) => { if (typeof rawConfig.noGraphQLTag === 'boolean') { return rawConfig.noGraphQLTag ? DocumentMode.documentNode : DocumentMode.graphQLTag; } return (0, utils_js_1.getConfigValue)(rawConfig.documentMode, DocumentMode.graphQLTag); })(rawConfig), importDocumentNodeExternallyFrom: (0, utils_js_1.getConfigValue)(rawConfig.importDocumentNodeExternallyFrom, ''), pureMagicComment: (0, utils_js_1.getConfigValue)(rawConfig.pureMagicComment, false), experimentalFragmentVariables: (0, utils_js_1.getConfigValue)(rawConfig.experimentalFragmentVariables, false), ...additionalConfig, }); this._schema = _schema; this._fragments = _fragments; this._collectedOperations = []; this._documents = []; this._additionalImports = []; this._imports = new Set(); this._documents = documents; (0, auto_bind_1.default)(this); } _extractFragments(document, withNested = false) { if (!document) { return []; } const names = new Set(); (0, plugin_helpers_1.oldVisit)(document, { enter: { FragmentSpread: (node) => { names.add(node.name.value); if (withNested) { const foundFragment = this._fragments.find(f => f.name === node.name.value); if (foundFragment) { const childItems = this._extractFragments(foundFragment.node, true); if (childItems && childItems.length > 0) { for (const item of childItems) { names.add(item); } } } } }, }, }); return Array.from(names); } _transformFragments(document) { const includeNestedFragments = this.config.documentMode === DocumentMode.documentNode || (this.config.dedupeFragments && document.kind === 'OperationDefinition'); return this._extractFragments(document, includeNestedFragments).map(document => this.getFragmentVariableName(document)); } _includeFragments(fragments, nodeKind) { if (fragments && fragments.length > 0) { if (this.config.documentMode === DocumentMode.documentNode) { return this._fragments .filter(f => fragments.includes(this.getFragmentVariableName(f.name))) .map(fragment => (0, graphql_1.print)(fragment.node)) .join('\n'); } if (this.config.documentMode === DocumentMode.documentNodeImportFragments) { return ''; } if (this.config.dedupeFragments && nodeKind !== 'OperationDefinition') { return ''; } return String(fragments.map(name => '${' + name + '}').join('\n')); } return ''; } _prepareDocument(documentStr) { return documentStr; } _gql(node) { const fragments = this._transformFragments(node); const doc = this._prepareDocument(` ${(0, graphql_1.print)(node).split('\\').join('\\\\') /* Re-escape escaped values in GraphQL syntax */} ${this._includeFragments(fragments, node.kind)}`); if (this.config.documentMode === DocumentMode.documentNode) { let gqlObj = (0, graphql_tag_1.default)([doc]); if (this.config.optimizeDocumentNode) { gqlObj = (0, optimize_1.optimizeDocumentNode)(gqlObj); } return JSON.stringify(gqlObj); } if (this.config.documentMode === DocumentMode.documentNodeImportFragments) { let gqlObj = (0, graphql_tag_1.default)([doc]); if (this.config.optimizeDocumentNode) { gqlObj = (0, optimize_1.optimizeDocumentNode)(gqlObj); } if (fragments.length > 0 && (!this.config.dedupeFragments || node.kind === 'OperationDefinition')) { const definitions = [ ...gqlObj.definitions.map(t => JSON.stringify(t)), ...fragments.map(name => `...${name}.definitions`), ].join(); return `{"kind":"${graphql_1.Kind.DOCUMENT}","definitions":[${definitions}]}`; } return JSON.stringify(gqlObj); } if (this.config.documentMode === DocumentMode.string) { return '`' + doc + '`'; } const gqlImport = this._parseImport(this.config.gqlImport || 'graphql-tag'); return (gqlImport.propName || 'gql') + '`' + doc + '`'; } _generateFragment(fragmentDocument) { const name = this.getFragmentVariableName(fragmentDocument); const fragmentTypeSuffix = this.getFragmentSuffix(fragmentDocument); return `export const ${name} =${this.config.pureMagicComment ? ' /*#__PURE__*/' : ''} ${this._gql(fragmentDocument)}${this.getDocumentNodeSignature(this.convertName(fragmentDocument.name.value, { useTypesPrefix: true, suffix: fragmentTypeSuffix, }), this.config.experimentalFragmentVariables ? this.convertName(fragmentDocument.name.value, { suffix: fragmentTypeSuffix + 'Variables', }) : 'unknown', fragmentDocument)};`; } get fragmentsGraph() { const graph = new dependency_graph_1.DepGraph({ circular: true }); for (const fragment of this._fragments) { if (graph.hasNode(fragment.name)) { const cachedAsString = (0, graphql_1.print)(graph.getNodeData(fragment.name).node); const asString = (0, graphql_1.print)(fragment.node); if (cachedAsString !== asString) { throw new Error(`Duplicated fragment called '${fragment.name}'!`); } } graph.addNode(fragment.name, fragment); } this._fragments.forEach(fragment => { const depends = this._extractFragments(fragment.node); if (depends && depends.length > 0) { depends.forEach(name => { graph.addDependency(fragment.name, name); }); } }); return graph; } get fragments() { if (this._fragments.length === 0 || this.config.documentMode === DocumentMode.external) { return ''; } const graph = this.fragmentsGraph; const orderedDeps = graph.overallOrder(); const localFragments = orderedDeps .filter(name => !graph.getNodeData(name).isExternal) .map(name => this._generateFragment(graph.getNodeData(name).node)); return localFragments.join('\n'); } _parseImport(importStr) { // This is a special case when we want to ignore importing, and just use `gql` provided from somewhere else // Plugins that uses that will need to ensure to add import/declaration for the gql identifier if (importStr === 'gql') { return { moduleName: null, propName: 'gql', }; } // This is a special use case, when we don't want this plugin to manage the import statement // of the gql tag. In this case, we provide something like `Namespace.gql` and it will be used instead. if (importStr.includes('.gql')) { return { moduleName: null, propName: importStr, }; } const [moduleName, propName] = importStr.split('#'); return { moduleName, propName, }; } _generateImport({ moduleName, propName }, varName, isTypeImport) { const typeImport = isTypeImport && this.config.useTypeImports ? 'import type' : 'import'; const propAlias = propName === varName ? '' : ` as ${varName}`; if (moduleName) { return `${typeImport} ${propName ? `{ ${propName}${propAlias} }` : varName} from '${moduleName}';`; } return null; } clearExtension(path) { const extension = (0, path_1.extname)(path); if (EXTENSIONS_TO_REMOVE.includes(extension)) { return path.replace(/\.[^/.]+$/, ''); } return path; } getImports(options = {}) { (this._additionalImports || []).forEach(i => this._imports.add(i)); switch (this.config.documentMode) { case DocumentMode.documentNode: case DocumentMode.documentNodeImportFragments: { const documentNodeImport = this._parseImport(this.config.documentNodeImport || 'graphql#DocumentNode'); const tagImport = this._generateImport(documentNodeImport, 'DocumentNode', true); if (tagImport) { this._imports.add(tagImport); } break; } case DocumentMode.graphQLTag: { const gqlImport = this._parseImport(this.config.gqlImport || 'graphql-tag'); const tagImport = this._generateImport(gqlImport, 'gql', false); if (tagImport) { this._imports.add(tagImport); } break; } case DocumentMode.external: { if (this._collectedOperations.length > 0) { if (this.config.importDocumentNodeExternallyFrom === 'near-operation-file' && this._documents.length === 1) { let documentPath = `./${this.clearExtension((0, path_1.basename)(this._documents[0].location))}`; if (!this.config.emitLegacyCommonJSImports) { documentPath += '.js'; } this._imports.add(`import * as Operations from '${documentPath}';`); } else { if (!this.config.importDocumentNodeExternallyFrom) { // eslint-disable-next-line no-console console.warn('importDocumentNodeExternallyFrom must be provided if documentMode=external'); } this._imports.add(`import * as Operations from '${this.clearExtension(this.config.importDocumentNodeExternallyFrom)}';`); } } break; } default: break; } if (!options.excludeFragments && !this.config.globalNamespace) { const { documentMode, fragmentImports } = this.config; if (documentMode === DocumentMode.graphQLTag || documentMode === DocumentMode.string || documentMode === DocumentMode.documentNodeImportFragments) { // keep track of what imports we've already generated so we don't try // to import the same identifier twice const alreadyImported = new Map(); const deduplicatedImports = fragmentImports .map(fragmentImport => { const { path, identifiers } = fragmentImport.importSource; if (!alreadyImported.has(path)) { alreadyImported.set(path, new Set()); } const alreadyImportedForPath = alreadyImported.get(path); const newIdentifiers = identifiers.filter(identifier => !alreadyImportedForPath.has(identifier.name)); newIdentifiers.forEach(newIdentifier => alreadyImportedForPath.add(newIdentifier.name)); // filter the set of identifiers in this fragment import to only // the ones we haven't already imported from this path return { ...fragmentImport, importSource: { ...fragmentImport.importSource, identifiers: newIdentifiers, }, emitLegacyCommonJSImports: this.config.emitLegacyCommonJSImports, }; }) // remove any imports that now have no identifiers in them .filter(fragmentImport => fragmentImport.importSource.identifiers.length > 0); deduplicatedImports.forEach(fragmentImport => { if (fragmentImport.outputPath !== fragmentImport.importSource.path) { this._imports.add((0, imports_js_1.generateFragmentImportStatement)(fragmentImport, 'document')); } }); } } return Array.from(this._imports); } buildOperation(_node, _documentVariableName, _operationType, _operationResultType, _operationVariablesTypes, _hasRequiredVariables) { return null; } getDocumentNodeSignature(_resultType, _variablesTypes, _node) { if (this.config.documentMode === DocumentMode.documentNode || this.config.documentMode === DocumentMode.documentNodeImportFragments) { return ` as unknown as DocumentNode`; } return ''; } /** * Checks if the specific operation has variables that are non-null (required), and also doesn't have default. * This is useful for deciding of `variables` should be optional or not. * @param node */ checkVariablesRequirements(node) { const variables = node.variableDefinitions || []; if (variables.length === 0) { return false; } return variables.some(variableDef => variableDef.type.kind === graphql_1.Kind.NON_NULL_TYPE && !variableDef.defaultValue); } getOperationVariableName(node) { return this.convertName(node, { suffix: this.config.documentVariableSuffix, prefix: this.config.documentVariablePrefix, useTypesPrefix: false, }); } OperationDefinition(node) { this._collectedOperations.push(node); const documentVariableName = this.getOperationVariableName(node); const operationType = (0, change_case_all_1.pascalCase)(node.operation); const operationTypeSuffix = this.getOperationSuffix(node, operationType); const operationResultType = this.convertName(node, { suffix: operationTypeSuffix + this._parsedConfig.operationResultSuffix, }); const operationVariablesTypes = this.convertName(node, { suffix: operationTypeSuffix + 'Variables', }); let documentString = ''; if (this.config.documentMode !== DocumentMode.external && documentVariableName !== '' // only generate exports for named queries ) { documentString = `${this.config.noExport ? '' : 'export'} const ${documentVariableName} =${this.config.pureMagicComment ? ' /*#__PURE__*/' : ''} ${this._gql(node)}${this.getDocumentNodeSignature(operationResultType, operationVariablesTypes, node)};`; } const hasRequiredVariables = this.checkVariablesRequirements(node); const additional = this.buildOperation(node, documentVariableName, operationType, operationResultType, operationVariablesTypes, hasRequiredVariables); return [documentString, additional].filter(a => a).join('\n'); } } exports.ClientSideBaseVisitor = ClientSideBaseVisitor;