1 | import addPlugin from '@graphql-codegen/add';
|
2 | import { join } from 'path';
|
3 | import { visit, Kind, print, buildASTSchema } from 'graphql';
|
4 | import parsePath from 'parse-filepath';
|
5 | import { isUsingTypes, DetailedError } from '@graphql-codegen/plugin-helpers';
|
6 | import { BaseVisitor, buildScalars, getConfigValue, getPossibleTypes, generateImportStatement, resolveImportSource } from '@graphql-codegen/visitor-plugin-common';
|
7 |
|
8 | function defineFilepathSubfolder(baseFilePath, folder) {
|
9 | const parsedPath = parsePath(baseFilePath);
|
10 | return join(parsedPath.dir, folder, parsedPath.base).replace(/\\/g, '/');
|
11 | }
|
12 | function appendExtensionToFilePath(baseFilePath, extension) {
|
13 | const parsedPath = parsePath(baseFilePath);
|
14 | return join(parsedPath.dir, parsedPath.name + extension).replace(/\\/g, '/');
|
15 | }
|
16 | function extractExternalFragmentsInUse(documentNode, fragmentNameToFile, result = {}, level = 0) {
|
17 | const ignoreList = new Set();
|
18 |
|
19 | visit(documentNode, {
|
20 | enter: {
|
21 | FragmentDefinition: (node) => {
|
22 | ignoreList.add(node.name.value);
|
23 | },
|
24 | },
|
25 | });
|
26 |
|
27 | visit(documentNode, {
|
28 | enter: {
|
29 | FragmentSpread: (node) => {
|
30 | if (!ignoreList.has(node.name.value)) {
|
31 | if (result[node.name.value] === undefined ||
|
32 | (result[node.name.value] !== undefined && level < result[node.name.value])) {
|
33 | result[node.name.value] = level;
|
34 | if (fragmentNameToFile[node.name.value]) {
|
35 | extractExternalFragmentsInUse(fragmentNameToFile[node.name.value].node, fragmentNameToFile, result, level + 1);
|
36 | }
|
37 | }
|
38 | }
|
39 | },
|
40 | },
|
41 | });
|
42 | return result;
|
43 | }
|
44 |
|
45 |
|
46 |
|
47 |
|
48 | function buildFragmentRegistry({ generateFilePath }, { documents, config }, schemaObject) {
|
49 | const baseVisitor = new BaseVisitor(config, {
|
50 | scalars: buildScalars(schemaObject, config.scalars),
|
51 | dedupeOperationSuffix: getConfigValue(config.dedupeOperationSuffix, false),
|
52 | omitOperationSuffix: getConfigValue(config.omitOperationSuffix, false),
|
53 | fragmentVariablePrefix: getConfigValue(config.fragmentVariablePrefix, ''),
|
54 | fragmentVariableSuffix: getConfigValue(config.fragmentVariableSuffix, 'FragmentDoc'),
|
55 | });
|
56 | const getFragmentImports = (possbileTypes, name) => {
|
57 | const fragmentImports = [];
|
58 | fragmentImports.push({ name: baseVisitor.getFragmentVariableName(name), kind: 'document' });
|
59 | const fragmentSuffix = baseVisitor.getFragmentSuffix(name);
|
60 | if (possbileTypes.length === 1) {
|
61 | fragmentImports.push({
|
62 | name: baseVisitor.convertName(name, {
|
63 | useTypesPrefix: true,
|
64 | suffix: fragmentSuffix,
|
65 | }),
|
66 | kind: 'type',
|
67 | });
|
68 | }
|
69 | else if (possbileTypes.length !== 0) {
|
70 | possbileTypes.forEach(typeName => {
|
71 | fragmentImports.push({
|
72 | name: baseVisitor.convertName(name, {
|
73 | useTypesPrefix: true,
|
74 | suffix: `_${typeName}_${fragmentSuffix}`,
|
75 | }),
|
76 | kind: 'type',
|
77 | });
|
78 | });
|
79 | }
|
80 | return fragmentImports;
|
81 | };
|
82 | const duplicateFragmentNames = [];
|
83 | const registry = documents.reduce((prev, documentRecord) => {
|
84 | const fragments = documentRecord.document.definitions.filter(d => d.kind === Kind.FRAGMENT_DEFINITION);
|
85 | if (fragments.length > 0) {
|
86 | for (const fragment of fragments) {
|
87 | const schemaType = schemaObject.getType(fragment.typeCondition.name.value);
|
88 | if (!schemaType) {
|
89 | throw new Error(`Fragment "${fragment.name.value}" is set on non-existing type "${fragment.typeCondition.name.value}"!`);
|
90 | }
|
91 | const possibleTypes = getPossibleTypes(schemaObject, schemaType);
|
92 | const filePath = generateFilePath(documentRecord.location);
|
93 | const imports = getFragmentImports(possibleTypes.map(t => t.name), fragment.name.value);
|
94 | if (prev[fragment.name.value] && print(fragment) !== print(prev[fragment.name.value].node)) {
|
95 | duplicateFragmentNames.push(fragment.name.value);
|
96 | }
|
97 | prev[fragment.name.value] = {
|
98 | filePath,
|
99 | imports,
|
100 | onType: fragment.typeCondition.name.value,
|
101 | node: fragment,
|
102 | };
|
103 | }
|
104 | }
|
105 | return prev;
|
106 | }, {});
|
107 | if (duplicateFragmentNames.length) {
|
108 | throw new Error(`Multiple fragments with the name(s) "${duplicateFragmentNames.join(', ')}" were found.`);
|
109 | }
|
110 | return registry;
|
111 | }
|
112 |
|
113 |
|
114 |
|
115 | function buildFragmentResolver(collectorOptions, presetOptions, schemaObject) {
|
116 | const fragmentRegistry = buildFragmentRegistry(collectorOptions, presetOptions, schemaObject);
|
117 | const { baseOutputDir } = presetOptions;
|
118 | const { baseDir, typesImport } = collectorOptions;
|
119 | function resolveFragments(generatedFilePath, documentFileContent) {
|
120 | const fragmentsInUse = extractExternalFragmentsInUse(documentFileContent, fragmentRegistry);
|
121 | const externalFragments = [];
|
122 |
|
123 | const fragmentFileImports = {};
|
124 | for (const fragmentName of Object.keys(fragmentsInUse)) {
|
125 | const level = fragmentsInUse[fragmentName];
|
126 | const fragmentDetails = fragmentRegistry[fragmentName];
|
127 | if (fragmentDetails) {
|
128 |
|
129 |
|
130 | if (level === 0) {
|
131 | if (fragmentFileImports[fragmentDetails.filePath] === undefined) {
|
132 | fragmentFileImports[fragmentDetails.filePath] = fragmentDetails.imports;
|
133 | }
|
134 | else {
|
135 | fragmentFileImports[fragmentDetails.filePath].push(...fragmentDetails.imports);
|
136 | }
|
137 | }
|
138 | externalFragments.push({
|
139 | level,
|
140 | isExternal: true,
|
141 | name: fragmentName,
|
142 | onType: fragmentDetails.onType,
|
143 | node: fragmentDetails.node,
|
144 | });
|
145 | }
|
146 | }
|
147 | return {
|
148 | externalFragments,
|
149 | fragmentImports: Object.entries(fragmentFileImports).map(([fragmentsFilePath, identifiers]) => ({
|
150 | baseDir,
|
151 | baseOutputDir,
|
152 | outputPath: generatedFilePath,
|
153 | importSource: {
|
154 | path: fragmentsFilePath,
|
155 | identifiers,
|
156 | },
|
157 | typesImport,
|
158 | })),
|
159 | };
|
160 | }
|
161 | return resolveFragments;
|
162 | }
|
163 |
|
164 |
|
165 |
|
166 |
|
167 |
|
168 |
|
169 |
|
170 | function resolveDocumentImports(presetOptions, schemaObject, importResolverOptions) {
|
171 | const resolveFragments = buildFragmentResolver(importResolverOptions, presetOptions, schemaObject);
|
172 | const { baseOutputDir, documents } = presetOptions;
|
173 | const { generateFilePath, schemaTypesSource, baseDir, typesImport } = importResolverOptions;
|
174 | return documents.map(documentFile => {
|
175 | try {
|
176 | const generatedFilePath = generateFilePath(documentFile.location);
|
177 | const importStatements = [];
|
178 | const { externalFragments, fragmentImports } = resolveFragments(generatedFilePath, documentFile.document);
|
179 | if (isUsingTypes(documentFile.document, externalFragments.map(m => m.name), schemaObject)) {
|
180 | const schemaTypesImportStatement = generateImportStatement({
|
181 | baseDir,
|
182 | importSource: resolveImportSource(schemaTypesSource),
|
183 | baseOutputDir,
|
184 | outputPath: generatedFilePath,
|
185 | typesImport,
|
186 | });
|
187 | importStatements.unshift(schemaTypesImportStatement);
|
188 | }
|
189 | return {
|
190 | filename: generatedFilePath,
|
191 | documents: [documentFile],
|
192 | importStatements,
|
193 | fragmentImports,
|
194 | externalFragments,
|
195 | };
|
196 | }
|
197 | catch (e) {
|
198 | throw new DetailedError(`Unable to validate GraphQL document!`, `
|
199 | File ${documentFile.location} caused error:
|
200 | ${e.message || e.toString()}
|
201 | `, documentFile.location);
|
202 | }
|
203 | });
|
204 | }
|
205 |
|
206 | const preset = {
|
207 | buildGeneratesSection: options => {
|
208 | var _a;
|
209 | const schemaObject = options.schemaAst
|
210 | ? options.schemaAst
|
211 | : buildASTSchema(options.schema, options.config);
|
212 | const baseDir = options.presetConfig.cwd || process.cwd();
|
213 | const extension = options.presetConfig.extension || '.generated.ts';
|
214 | const folder = options.presetConfig.folder || '';
|
215 | const importTypesNamespace = options.presetConfig.importTypesNamespace || 'Types';
|
216 | const importAllFragmentsFrom = options.presetConfig.importAllFragmentsFrom || null;
|
217 | const baseTypesPath = options.presetConfig.baseTypesPath;
|
218 | if (!baseTypesPath) {
|
219 | throw new Error(`Preset "near-operation-file" requires you to specify "baseTypesPath" configuration and point it to your base types file (generated by "typescript" plugin)!`);
|
220 | }
|
221 | const shouldAbsolute = !baseTypesPath.startsWith('~');
|
222 | const pluginMap = {
|
223 | ...options.pluginMap,
|
224 | add: addPlugin,
|
225 | };
|
226 | const sources = resolveDocumentImports(options, schemaObject, {
|
227 | baseDir,
|
228 | generateFilePath(location) {
|
229 | const newFilePath = defineFilepathSubfolder(location, folder);
|
230 | return appendExtensionToFilePath(newFilePath, extension);
|
231 | },
|
232 | schemaTypesSource: {
|
233 | path: shouldAbsolute ? join(options.baseOutputDir, baseTypesPath) : baseTypesPath,
|
234 | namespace: importTypesNamespace,
|
235 | },
|
236 | typesImport: (_a = options.config.useTypeImports) !== null && _a !== void 0 ? _a : false,
|
237 | });
|
238 | return sources.map(({ importStatements, externalFragments, fragmentImports, ...source }) => {
|
239 | let fragmentImportsArr = fragmentImports;
|
240 | if (importAllFragmentsFrom) {
|
241 | fragmentImportsArr = fragmentImports.map(t => {
|
242 | const newImportSource = typeof importAllFragmentsFrom === 'string'
|
243 | ? { ...t.importSource, path: importAllFragmentsFrom }
|
244 | : importAllFragmentsFrom(t.importSource, source.filename);
|
245 | return {
|
246 | ...t,
|
247 | importSource: newImportSource || t.importSource,
|
248 | };
|
249 | });
|
250 | }
|
251 | const plugins = [
|
252 |
|
253 | ...(options.config.globalNamespace
|
254 | ? []
|
255 | : importStatements.map(importStatement => ({ add: { content: importStatement } }))),
|
256 | ...options.plugins,
|
257 | ];
|
258 | const config = {
|
259 | ...options.config,
|
260 |
|
261 |
|
262 | exportFragmentSpreadSubTypes: true,
|
263 | namespacedImportName: importTypesNamespace,
|
264 | externalFragments,
|
265 | fragmentImports: fragmentImportsArr,
|
266 | };
|
267 | return {
|
268 | ...source,
|
269 | plugins,
|
270 | pluginMap,
|
271 | config,
|
272 | schema: options.schema,
|
273 | schemaAst: schemaObject,
|
274 | skipDocumentsValidation: true,
|
275 | };
|
276 | });
|
277 | },
|
278 | };
|
279 |
|
280 | export default preset;
|
281 | export { preset, resolveDocumentImports };
|
282 |
|