UNPKG

12.9 kBJavaScriptView Raw
1import addPlugin from '@graphql-codegen/add';
2import { join } from 'path';
3import { visit, Kind, print, buildASTSchema } from 'graphql';
4import parsePath from 'parse-filepath';
5import { isUsingTypes, DetailedError } from '@graphql-codegen/plugin-helpers';
6import { BaseVisitor, buildScalars, getConfigValue, getPossibleTypes, generateImportStatement, resolveImportSource } from '@graphql-codegen/visitor-plugin-common';
7
8function defineFilepathSubfolder(baseFilePath, folder) {
9 const parsedPath = parsePath(baseFilePath);
10 return join(parsedPath.dir, folder, parsedPath.base).replace(/\\/g, '/');
11}
12function appendExtensionToFilePath(baseFilePath, extension) {
13 const parsedPath = parsePath(baseFilePath);
14 return join(parsedPath.dir, parsedPath.name + extension).replace(/\\/g, '/');
15}
16function extractExternalFragmentsInUse(documentNode, fragmentNameToFile, result = {}, level = 0) {
17 const ignoreList = new Set();
18 // First, take all fragments definition from the current file, and mark them as ignored
19 visit(documentNode, {
20 enter: {
21 FragmentDefinition: (node) => {
22 ignoreList.add(node.name.value);
23 },
24 },
25 });
26 // Then, look for all used fragments in this document
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 * Used by `buildFragmentResolver` to build a mapping of fragmentNames to paths, importNames, and other useful info
47 */
48function 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 * Builds a fragment "resolver" that collects `externalFragments` definitions and `fragmentImportStatements`
114 */
115function 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 // fragment files to import names
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 // add top level references to the import object
129 // we don't checkf or global namespace because the calling config can do so
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 * Transform the preset's provided documents into single-file generator sources, while resolving fragment and user-defined imports
166 *
167 * Resolves user provided imports and fragment imports using the `DocumentImportResolverOptions`.
168 * Does not define specific plugins, but rather returns a string[] of `importStatements` for the calling plugin to make use of
169 */
170function 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
206const 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 // TODO/NOTE I made globalNamespace include schema types - is that correct?
253 ...(options.config.globalNamespace
254 ? []
255 : importStatements.map(importStatement => ({ add: { content: importStatement } }))),
256 ...options.plugins,
257 ];
258 const config = {
259 ...options.config,
260 // This is set here in order to make sure the fragment spreads sub types
261 // are exported from operations file
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
280export default preset;
281export { preset, resolveDocumentImports };
282//# sourceMappingURL=index.esm.js.map