@graphcommerce/graphql-codegen-near-operation-file 2.102.3 → 2.103.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,18 +1,19 @@
1
1
  {
2
2
  "name": "@graphcommerce/graphql-codegen-near-operation-file",
3
- "version": "2.102.3",
3
+ "version": "2.103.3",
4
4
  "sideEffects": false,
5
5
  "main": "dist/index.js",
6
6
  "engines": {
7
7
  "node": "14.x"
8
8
  },
9
9
  "files": [
10
- "dist"
10
+ "dist",
11
+ "src"
11
12
  ],
12
13
  "scripts": {
13
14
  "dev": "tsc --preserveWatchOutput --watch --sourceMap --outDir dist",
14
15
  "build": "tsc --target es5 --outDir dist",
15
- "prepare": "yarn build"
16
+ "prepack": "yarn build"
16
17
  },
17
18
  "prettier": "@graphcommerce/prettier-config-pwa",
18
19
  "browserslist": [
@@ -26,9 +27,9 @@
26
27
  },
27
28
  "devDependencies": {
28
29
  "@graphcommerce/browserslist-config-pwa": "^3.0.1",
29
- "@graphcommerce/eslint-config-pwa": "^3.0.2",
30
+ "@graphcommerce/eslint-config-pwa": "^3.0.5",
30
31
  "@graphcommerce/prettier-config-pwa": "^3.0.2",
31
- "@graphcommerce/typescript-config-pwa": "^3.0.1",
32
+ "@graphcommerce/typescript-config-pwa": "^3.1.0",
32
33
  "@playwright/test": "^1.15.0"
33
34
  },
34
35
  "dependencies": {
@@ -39,5 +40,5 @@
39
40
  "graphql": "^15.6.0",
40
41
  "parse-filepath": "^1.0.2"
41
42
  },
42
- "gitHead": "aba0a6340e2d098313f1c25cc2047c18e5ea5db5"
43
+ "gitHead": "f1d0e488921787f39a260c410790ec13d32c8143"
43
44
  }
@@ -0,0 +1,191 @@
1
+ /* eslint-disable import/no-cycle */
2
+ import { Types } from '@graphql-codegen/plugin-helpers'
3
+ import {
4
+ BaseVisitor,
5
+ FragmentImport,
6
+ getConfigValue,
7
+ getPossibleTypes,
8
+ LoadedFragment,
9
+ ParsedConfig,
10
+ RawConfig,
11
+ ImportDeclaration,
12
+ buildScalarsFromConfig,
13
+ } from '@graphql-codegen/visitor-plugin-common'
14
+ import { DocumentNode, FragmentDefinitionNode, GraphQLSchema, Kind, print } from 'graphql'
15
+ import { DocumentImportResolverOptions } from './resolve-document-imports'
16
+ import { extractExternalFragmentsInUse } from './utils'
17
+
18
+ export interface NearOperationFileParsedConfig extends ParsedConfig {
19
+ importTypesNamespace?: string
20
+ dedupeOperationSuffix: boolean
21
+ omitOperationSuffix: boolean
22
+ fragmentVariablePrefix: string
23
+ fragmentVariableSuffix: string
24
+ }
25
+
26
+ export type FragmentRegistry = {
27
+ [fragmentName: string]: {
28
+ filePath: string
29
+ onType: string
30
+ node: FragmentDefinitionNode
31
+ imports: Array<FragmentImport>
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Used by `buildFragmentResolver` to build a mapping of fragmentNames to paths, importNames, and
37
+ * other useful info
38
+ */
39
+ export function buildFragmentRegistry(
40
+ { generateFilePath }: DocumentImportResolverOptions,
41
+ { documents, config }: Types.PresetFnArgs<{}>,
42
+ schemaObject: GraphQLSchema,
43
+ ): FragmentRegistry {
44
+ const baseVisitor = new BaseVisitor<RawConfig, NearOperationFileParsedConfig>(config, {
45
+ scalars: buildScalarsFromConfig(schemaObject, config),
46
+ dedupeOperationSuffix: getConfigValue(config.dedupeOperationSuffix, false),
47
+ omitOperationSuffix: getConfigValue(config.omitOperationSuffix, false),
48
+ fragmentVariablePrefix: getConfigValue(config.fragmentVariablePrefix, ''),
49
+ fragmentVariableSuffix: getConfigValue(config.fragmentVariableSuffix, 'FragmentDoc'),
50
+ })
51
+
52
+ const getFragmentImports = (possbileTypes: string[], name: string): Array<FragmentImport> => {
53
+ const fragmentImports: Array<FragmentImport> = []
54
+
55
+ fragmentImports.push({ name: baseVisitor.getFragmentVariableName(name), kind: 'document' })
56
+
57
+ const fragmentSuffix = baseVisitor.getFragmentSuffix(name)
58
+ if (possbileTypes.length === 1) {
59
+ fragmentImports.push({
60
+ name: baseVisitor.convertName(name, {
61
+ useTypesPrefix: true,
62
+ suffix: fragmentSuffix,
63
+ }),
64
+ kind: 'type',
65
+ })
66
+ } else if (possbileTypes.length !== 0) {
67
+ possbileTypes.forEach((typeName) => {
68
+ fragmentImports.push({
69
+ name: baseVisitor.convertName(name, {
70
+ useTypesPrefix: true,
71
+ suffix: `_${typeName}_${fragmentSuffix}`,
72
+ }),
73
+ kind: 'type',
74
+ })
75
+ })
76
+ }
77
+
78
+ return fragmentImports
79
+ }
80
+
81
+ const duplicateFragmentNames: string[] = []
82
+ const registry = documents.reduce<FragmentRegistry>((prev: FragmentRegistry, documentRecord) => {
83
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
84
+ const fragments: FragmentDefinitionNode[] = documentRecord.document!.definitions.filter(
85
+ (d) => d.kind === Kind.FRAGMENT_DEFINITION,
86
+ ) as FragmentDefinitionNode[]
87
+
88
+ if (fragments.length > 0) {
89
+ for (const fragment of fragments) {
90
+ const schemaType = schemaObject.getType(fragment.typeCondition.name.value)
91
+
92
+ if (!schemaType) {
93
+ throw new Error(
94
+ `Fragment "${fragment.name.value}" is set on non-existing type "${fragment.typeCondition.name.value}"!`,
95
+ )
96
+ }
97
+
98
+ const possibleTypes = getPossibleTypes(schemaObject, schemaType)
99
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
100
+ const filePath = generateFilePath(documentRecord.location!)
101
+ const imports = getFragmentImports(
102
+ possibleTypes.map((t) => t.name),
103
+ fragment.name.value,
104
+ )
105
+
106
+ if (
107
+ prev[fragment.name.value] &&
108
+ print(fragment) !== print(prev[fragment.name.value].node)
109
+ ) {
110
+ duplicateFragmentNames.push(fragment.name.value)
111
+ }
112
+
113
+ prev[fragment.name.value] = {
114
+ filePath,
115
+ imports,
116
+ onType: fragment.typeCondition.name.value,
117
+ node: fragment,
118
+ }
119
+ }
120
+ }
121
+
122
+ return prev
123
+ }, {})
124
+
125
+ if (duplicateFragmentNames.length) {
126
+ throw new Error(
127
+ `Multiple fragments with the name(s) "${duplicateFragmentNames.join(', ')}" were found.`,
128
+ )
129
+ }
130
+
131
+ return registry
132
+ }
133
+
134
+ /** Builds a fragment "resolver" that collects `externalFragments` definitions and `fragmentImportStatements` */
135
+ export default function buildFragmentResolver<T>(
136
+ collectorOptions: DocumentImportResolverOptions,
137
+ presetOptions: Types.PresetFnArgs<T>,
138
+ schemaObject: GraphQLSchema,
139
+ ) {
140
+ const fragmentRegistry = buildFragmentRegistry(collectorOptions, presetOptions, schemaObject)
141
+ const { baseOutputDir } = presetOptions
142
+ const { baseDir, typesImport } = collectorOptions
143
+
144
+ function resolveFragments(generatedFilePath: string, documentFileContent: DocumentNode) {
145
+ const fragmentsInUse = extractExternalFragmentsInUse(documentFileContent, fragmentRegistry)
146
+
147
+ const externalFragments: LoadedFragment<{ level: number }>[] = []
148
+ // fragment files to import names
149
+ const fragmentFileImports: { [fragmentFile: string]: Array<FragmentImport> } = {}
150
+ for (const fragmentName of Object.keys(fragmentsInUse)) {
151
+ const level = fragmentsInUse[fragmentName]
152
+ const fragmentDetails = fragmentRegistry[fragmentName]
153
+ if (fragmentDetails) {
154
+ // add top level references to the import object
155
+ // we don't checkf or global namespace because the calling config can do so
156
+ if (level === 0) {
157
+ if (fragmentFileImports[fragmentDetails.filePath] === undefined) {
158
+ fragmentFileImports[fragmentDetails.filePath] = fragmentDetails.imports
159
+ } else {
160
+ fragmentFileImports[fragmentDetails.filePath].push(...fragmentDetails.imports)
161
+ }
162
+ }
163
+
164
+ externalFragments.push({
165
+ level,
166
+ isExternal: true,
167
+ name: fragmentName,
168
+ onType: fragmentDetails.onType,
169
+ node: fragmentDetails.node,
170
+ })
171
+ }
172
+ }
173
+
174
+ return {
175
+ externalFragments,
176
+ fragmentImports: Object.entries(fragmentFileImports).map(
177
+ ([fragmentsFilePath, identifiers]): ImportDeclaration<FragmentImport> => ({
178
+ baseDir,
179
+ baseOutputDir,
180
+ outputPath: generatedFilePath,
181
+ importSource: {
182
+ path: fragmentsFilePath,
183
+ identifiers,
184
+ },
185
+ typesImport,
186
+ }),
187
+ ),
188
+ }
189
+ }
190
+ return resolveFragments
191
+ }
package/src/index.ts ADDED
@@ -0,0 +1,266 @@
1
+ import { join } from 'path'
2
+ import addPlugin from '@graphql-codegen/add'
3
+ import { Types, CodegenPlugin } from '@graphql-codegen/plugin-helpers'
4
+ import {
5
+ FragmentImport,
6
+ ImportDeclaration,
7
+ ImportSource,
8
+ } from '@graphql-codegen/visitor-plugin-common'
9
+ import { FragmentDefinitionNode, buildASTSchema, visit } from 'graphql'
10
+ import { injectInjectables } from './injectable'
11
+ import { resolveDocumentImports, DocumentImportResolverOptions } from './resolve-document-imports'
12
+ import { appendExtensionToFilePath, defineFilepathSubfolder } from './utils'
13
+
14
+ export { resolveDocumentImports }
15
+ export type { DocumentImportResolverOptions }
16
+
17
+ export type FragmentImportFromFn = (
18
+ source: ImportSource<FragmentImport>,
19
+ sourceFilePath: string,
20
+ ) => ImportSource<FragmentImport>
21
+
22
+ export type NearOperationFileConfig = {
23
+ /**
24
+ * Required, should point to the base schema types file. The key of the output is used a the base
25
+ * path for this file.
26
+ *
27
+ * If you wish to use an NPM package or a local workspace package, make sure to prefix the package
28
+ * name with `~`.
29
+ *
30
+ * @exampleMarkdown ```yml generates:
31
+ * src/:
32
+ * preset: near-operation-file
33
+ * presetConfig:
34
+ * baseTypesPath: types.ts
35
+ * plugins:
36
+ * - typescript-operations
37
+ * ```
38
+ */
39
+ baseTypesPath: string
40
+ /**
41
+ * Overrides all external fragments import types by using a specific file path or a package name.
42
+ *
43
+ * If you wish to use an NPM package or a local workspace package, make sure to prefix the package
44
+ * name with `~`.
45
+ *
46
+ * @exampleMarkdown ```yml generates:
47
+ * src/:
48
+ * preset: near-operation-file
49
+ * presetConfig:
50
+ * baseTypesPath: types.ts
51
+ * importAllFragmentsFrom: '~types'
52
+ * plugins:
53
+ * - typescript-operations
54
+ * ```
55
+ */
56
+ importAllFragmentsFrom?: string | FragmentImportFromFn
57
+ /**
58
+ * Optional, sets the extension for the generated files. Use this to override the extension if you
59
+ * are using plugins that requires a different type of extensions (such as `typescript-react-apollo`)
60
+ *
61
+ * @default .generates.ts
62
+ * @exampleMarkdown ```yml generates:
63
+ * src/:
64
+ * preset: near-operation-file
65
+ * presetConfig:
66
+ * baseTypesPath: types.ts
67
+ * extension: .generated.tsx
68
+ * plugins:
69
+ * - typescript-operations
70
+ * - typescript-react-apollo
71
+ * ```
72
+ */
73
+ extension?: string
74
+ /**
75
+ * Optional, override the `cwd` of the execution. We are using `cwd` to figure out the imports
76
+ * between files. Use this if your execuion path is not your project root directory.
77
+ *
78
+ * @default process.cwd()
79
+ * @exampleMarkdown ```yml generates:
80
+ * src/:
81
+ * preset: near-operation-file
82
+ * presetConfig:
83
+ * baseTypesPath: types.ts
84
+ * cwd: /some/path
85
+ * plugins:
86
+ * - typescript-operations
87
+ * ```
88
+ */
89
+ cwd?: string
90
+ /**
91
+ * Optional, defines a folder, (Relative to the source files) where the generated files will be created.
92
+ *
93
+ * @default ''
94
+ * @exampleMarkdown ```yml generates:
95
+ * src/:
96
+ * preset: near-operation-file
97
+ * presetConfig:
98
+ * baseTypesPath: types.ts
99
+ * folder: __generated__
100
+ * plugins:
101
+ * - typescript-operations
102
+ * ```
103
+ */
104
+ folder?: string
105
+ /**
106
+ * Optional, override the name of the import namespace used to import from the `baseTypesPath` file.
107
+ *
108
+ * @default Types
109
+ * @exampleMarkdown ```yml generates:
110
+ * src/:
111
+ * preset: near-operation-file
112
+ * presetConfig:
113
+ * baseTypesPath: types.ts
114
+ * importTypesNamespace: SchemaTypes
115
+ * plugins:
116
+ * - typescript-operations
117
+ * ```
118
+ */
119
+ importTypesNamespace?: string
120
+
121
+ /**
122
+ * Enable the injectables
123
+ *
124
+ * ```yml
125
+ * src/:
126
+ * preset: near-operation-file
127
+ * presetConfig:
128
+ * baseTypesPath: types.ts
129
+ * injectables: true
130
+ * plugins:
131
+ * - typescript-operations
132
+ * ```
133
+ */
134
+ injectables?: boolean
135
+ }
136
+
137
+ export type FragmentNameToFile = {
138
+ [fragmentName: string]: {
139
+ location: string
140
+ importsNames: string[]
141
+ onType: string
142
+ node: FragmentDefinitionNode
143
+ }
144
+ }
145
+
146
+ function isFragment(documentFile: Types.DocumentFile) {
147
+ let name = false
148
+
149
+ visit(documentFile.document!, {
150
+ enter: {
151
+ FragmentDefinition: () => {
152
+ name = true
153
+ },
154
+ },
155
+ })
156
+ return name
157
+ }
158
+
159
+ function isDocument(documentFiles: Types.DocumentFile[]) {
160
+ return !documentFiles.every(isFragment)
161
+ }
162
+
163
+ export const preset: Types.OutputPreset<NearOperationFileConfig> = {
164
+ buildGeneratesSection: (options) => {
165
+ if (options.presetConfig.injectables) {
166
+ options.documents = injectInjectables(options.documents)
167
+ }
168
+
169
+ const schemaObject = options.schemaAst ?? buildASTSchema(options.schema, options.config)
170
+
171
+ const baseDir = options.presetConfig.cwd ?? process.cwd()
172
+ const extension = options.presetConfig.extension ?? '.generated.ts'
173
+ const folder = options.presetConfig.folder ?? ''
174
+ const importTypesNamespace = options.presetConfig.importTypesNamespace ?? 'Types'
175
+ const { importAllFragmentsFrom } = options.presetConfig
176
+
177
+ const { baseTypesPath } = options.presetConfig
178
+
179
+ if (!baseTypesPath) {
180
+ throw new Error(
181
+ `Preset "near-operation-file" requires you to specify "baseTypesPath" configuration and point it to your base types file (generated by "typescript" plugin)!`,
182
+ )
183
+ }
184
+
185
+ const shouldAbsolute = !baseTypesPath.startsWith('~')
186
+
187
+ const pluginMap: { [name: string]: CodegenPlugin } = {
188
+ ...options.pluginMap,
189
+ add: addPlugin,
190
+ }
191
+
192
+ const sources = resolveDocumentImports(options, schemaObject, {
193
+ baseDir,
194
+ generateFilePath(location: string) {
195
+ const newFilePath = defineFilepathSubfolder(location, folder)
196
+
197
+ return appendExtensionToFilePath(newFilePath, extension)
198
+ },
199
+ schemaTypesSource: {
200
+ path: shouldAbsolute ? join(options.baseOutputDir, baseTypesPath) : baseTypesPath,
201
+ namespace: importTypesNamespace,
202
+ },
203
+ typesImport: options.config.useTypeImports ?? false,
204
+ })
205
+
206
+ return sources.map<Types.GenerateOptions>(
207
+ ({ importStatements, externalFragments, fragmentImports, documents, ...source }) => {
208
+ let fragmentImportsArr = fragmentImports
209
+
210
+ if (importAllFragmentsFrom) {
211
+ fragmentImportsArr = fragmentImports.map<ImportDeclaration<FragmentImport>>((t) => {
212
+ const newImportSource: ImportSource<FragmentImport> =
213
+ typeof importAllFragmentsFrom === 'string'
214
+ ? { ...t.importSource, path: importAllFragmentsFrom }
215
+ : importAllFragmentsFrom(t.importSource, source.filename)
216
+
217
+ return {
218
+ ...t,
219
+ importSource: newImportSource || t.importSource,
220
+ }
221
+ })
222
+ }
223
+
224
+ const isDoc = isDocument(documents)
225
+ const isRelayOptimizer = !!Object.keys(pluginMap).find((plugin) =>
226
+ plugin.includes('relay-optimizer-plugin'),
227
+ )
228
+
229
+ const plugins = [
230
+ // TODO/NOTE I made globalNamespace include schema types - is that correct?
231
+ ...(options.config.globalNamespace
232
+ ? []
233
+ : importStatements.map((importStatement) => ({ add: { content: importStatement } }))),
234
+ ...options.plugins.filter(
235
+ (pluginOptions) =>
236
+ !isRelayOptimizer ||
237
+ isDoc ||
238
+ !Object.keys(pluginOptions).includes('typed-document-node'),
239
+ ),
240
+ ]
241
+ const config = {
242
+ ...options.config,
243
+ // This is set here in order to make sure the fragment spreads sub types
244
+ // are exported from operations file
245
+ exportFragmentSpreadSubTypes: true,
246
+ namespacedImportName: importTypesNamespace,
247
+ externalFragments,
248
+ fragmentImports: fragmentImportsArr,
249
+ }
250
+
251
+ return {
252
+ ...source,
253
+ documents,
254
+ plugins,
255
+ pluginMap,
256
+ config,
257
+ schema: options.schema,
258
+ schemaAst: schemaObject,
259
+ skipDocumentsValidation: true,
260
+ }
261
+ },
262
+ )
263
+ },
264
+ }
265
+
266
+ export default preset
@@ -0,0 +1,21 @@
1
+ """
2
+ Defines wheter a Fragment can be injected
3
+
4
+ ```graphql
5
+ fragment MyInjectableFragment on Model @injectable {
6
+ id
7
+ }
8
+ ```
9
+ """
10
+ directive @injectable on FRAGMENT_DEFINITION
11
+
12
+ """
13
+ Defines whether a Fragment injects into an @injectable
14
+
15
+ ```graphql
16
+ fragment MyFragment on Model @inject(into ["MyInjectableFragment"]) {
17
+ field
18
+ }
19
+ ```
20
+ """
21
+ directive @inject(into: [String!]!) on FRAGMENT_DEFINITION
@@ -0,0 +1,126 @@
1
+ /* eslint-disable react/destructuring-assignment */
2
+ import { Types } from '@graphql-codegen/plugin-helpers'
3
+ import { visit, DocumentNode, FragmentSpreadNode, FragmentDefinitionNode } from 'graphql'
4
+
5
+ function isFragment(document: DocumentNode) {
6
+ let is = false
7
+ visit(document, {
8
+ FragmentDefinition: () => {
9
+ is = true
10
+ },
11
+ })
12
+ return is
13
+ }
14
+
15
+ function hasInjectableDirective(document: DocumentNode) {
16
+ let is = false
17
+ visit(document, {
18
+ Directive: (node) => {
19
+ if (!is && node.name.value === 'injectable') is = true
20
+ },
21
+ })
22
+ return is && isFragment
23
+ }
24
+
25
+ function hasInjectDirective(document: DocumentNode) {
26
+ let is = false
27
+ visit(document, {
28
+ Directive: (node) => {
29
+ if (!is && node.name.value === 'inject') is = true
30
+ },
31
+ })
32
+ return is && isFragment
33
+ }
34
+
35
+ type Inject = { into: string[]; fragment: FragmentDefinitionNode }
36
+
37
+ function throwInjectError(conf: Partial<Inject>, message: string) {
38
+ const val = conf.into?.map((v) => `"${v}"`)
39
+
40
+ throw Error(
41
+ `${message}
42
+ fragment ${conf.fragment?.name.value} on ${conf.fragment?.typeCondition.name.value} @inject(into: [${val}]) { ... }`,
43
+ )
44
+ }
45
+
46
+ function assertValidInject(injectVal: Partial<Inject>): asserts injectVal is Inject {
47
+ const { into, fragment } = injectVal
48
+ if (!fragment || into?.length === 0) throwInjectError(injectVal, 'Invalid inject')
49
+ }
50
+
51
+ function getInjectConf(document: DocumentNode): Inject {
52
+ if (!hasInjectDirective(document)) throw Error('')
53
+
54
+ const conf: Partial<Inject> = { into: [] }
55
+ visit(document, {
56
+ Directive: (node) => {
57
+ if (node.name.value !== 'inject') return false
58
+ visit(node, {
59
+ Argument: (arg) => {
60
+ if (arg.name.value !== 'into') return false
61
+ visit(arg, {
62
+ ListValue: (list) => {
63
+ list.values.forEach((value) => {
64
+ visit(value, {
65
+ StringValue: (string) => {
66
+ conf.into?.push(string.value)
67
+ },
68
+ })
69
+ })
70
+ },
71
+ })
72
+ return undefined
73
+ },
74
+ })
75
+ return null
76
+ },
77
+ FragmentDefinition: (node) => {
78
+ conf.fragment = node
79
+ },
80
+ })
81
+ assertValidInject(conf)
82
+ return conf
83
+ }
84
+
85
+ function injectInjectable(injectables: DocumentNode[], injector: DocumentNode) {
86
+ const injectVal = getInjectConf(injector)
87
+ const { into, fragment } = injectVal
88
+
89
+ into.forEach((target) => {
90
+ let found = false
91
+ injectables.forEach((injectable) => {
92
+ visit(injectable, {
93
+ FragmentDefinition: (frag) => {
94
+ if (frag.name.value === target) {
95
+ found = true
96
+
97
+ const spread: FragmentSpreadNode = {
98
+ kind: 'FragmentSpread',
99
+ name: { kind: 'Name', value: fragment.name.value },
100
+ }
101
+ frag.selectionSet.selections = [...frag.selectionSet.selections, spread]
102
+ }
103
+ },
104
+ })
105
+ })
106
+ if (!found)
107
+ throwInjectError(
108
+ injectVal,
109
+ `fragment ${target} @injectable { ... } can not be found or isn't injectable`,
110
+ )
111
+ })
112
+ }
113
+
114
+ export function injectInjectables(documentFiles: Types.DocumentFile[]) {
115
+ const documents = documentFiles
116
+ .map(({ document }) => document)
117
+ .filter((doc) => doc) as DocumentNode[]
118
+
119
+ const injectables = documents.filter((d) => isFragment(d) && hasInjectableDirective(d))
120
+
121
+ const injectors = documents.filter((d) => isFragment(d) && hasInjectDirective(d))
122
+
123
+ injectors.forEach((d) => injectInjectable(injectables, d))
124
+
125
+ return documentFiles
126
+ }
@@ -0,0 +1,173 @@
1
+ /* eslint-disable import/no-cycle */
2
+ import { resolve } from 'path'
3
+ import { isUsingTypes, Types, DetailedError } from '@graphql-codegen/plugin-helpers'
4
+ import {
5
+ generateImportStatement,
6
+ ImportSource,
7
+ resolveImportSource,
8
+ FragmentImport,
9
+ ImportDeclaration,
10
+ LoadedFragment,
11
+ } from '@graphql-codegen/visitor-plugin-common'
12
+ import { Source } from '@graphql-tools/utils'
13
+ import { FragmentDefinitionNode, GraphQLSchema, visit } from 'graphql'
14
+ import buildFragmentResolver, { buildFragmentRegistry } from './fragment-resolver'
15
+ import { extractExternalFragmentsInUse } from './utils'
16
+
17
+ export type FragmentRegistry = {
18
+ [fragmentName: string]: {
19
+ location: string
20
+ importNames: string[]
21
+ onType: string
22
+ node: FragmentDefinitionNode
23
+ }
24
+ }
25
+
26
+ export type DocumentImportResolverOptions = {
27
+ baseDir: string
28
+ /** Generates a target file path from the source `document.location` */
29
+ generateFilePath: (location: string) => string
30
+ /** Schema base types source */
31
+ schemaTypesSource: string | ImportSource
32
+ /** Should `import type` be used */
33
+ typesImport: boolean
34
+ }
35
+
36
+ interface ResolveDocumentImportResult {
37
+ filename: string
38
+ documents: Source[]
39
+ importStatements: string[]
40
+ fragmentImports: ImportDeclaration<FragmentImport>[]
41
+ externalFragments: LoadedFragment<{
42
+ level: number
43
+ }>[]
44
+ }
45
+
46
+ function getFragmentName(documentFile: Types.DocumentFile) {
47
+ let name: string | undefined
48
+ visit(documentFile.document!, {
49
+ enter: {
50
+ FragmentDefinition: (node: FragmentDefinitionNode) => {
51
+ name = node.name.value
52
+ },
53
+ },
54
+ })
55
+ return name
56
+ }
57
+
58
+ /**
59
+ * Transform the preset's provided documents into single-file generator sources, while resolving
60
+ * fragment and user-defined imports
61
+ *
62
+ * Resolves user provided imports and fragment imports using the `DocumentImportResolverOptions`.
63
+ * Does not define specific plugins, but rather returns a string[] of `importStatements` for the
64
+ * calling plugin to make use of
65
+ */
66
+ export function resolveDocumentImports<T>(
67
+ presetOptions: Types.PresetFnArgs<T>,
68
+ schemaObject: GraphQLSchema,
69
+ importResolverOptions: DocumentImportResolverOptions,
70
+ ): Array<ResolveDocumentImportResult> {
71
+ const { baseOutputDir, documents, pluginMap } = presetOptions
72
+ const { generateFilePath, schemaTypesSource, baseDir, typesImport } = importResolverOptions
73
+
74
+ const resolveFragments = buildFragmentResolver(importResolverOptions, presetOptions, schemaObject)
75
+ const fragmentRegistry = buildFragmentRegistry(importResolverOptions, presetOptions, schemaObject)
76
+
77
+ const isRelayOptimizer = !!Object.keys(pluginMap).find((plugin) =>
78
+ plugin.includes('relay-optimizer-plugin'),
79
+ )
80
+
81
+ const resDocuments = documents.map((documentFile) => {
82
+ try {
83
+ const isFragment = typeof getFragmentName(documentFile) !== 'undefined'
84
+
85
+ if (!isFragment && isRelayOptimizer) {
86
+ const generatedFilePath = generateFilePath(documentFile.location!)
87
+
88
+ let externalFragments = extractExternalFragmentsInUse(
89
+ documentFile.document!,
90
+ fragmentRegistry,
91
+ )
92
+ // Sort the entries in the right order so fragments are defined when using
93
+ externalFragments = Object.fromEntries(
94
+ Object.entries(externalFragments).sort(([, levelA], [, levelB]) => levelB - levelA),
95
+ )
96
+
97
+ const fragments = documents.filter(
98
+ (d) => typeof externalFragments[getFragmentName(d) ?? ''] !== 'undefined',
99
+ )
100
+
101
+ const importStatements: string[] = []
102
+
103
+ if (isUsingTypes(documentFile.document!, [], schemaObject)) {
104
+ const schemaTypesImportStatement = generateImportStatement({
105
+ baseDir,
106
+ importSource: resolveImportSource(schemaTypesSource),
107
+ baseOutputDir,
108
+ outputPath: generatedFilePath,
109
+ typesImport,
110
+ })
111
+ importStatements.unshift(schemaTypesImportStatement)
112
+ }
113
+
114
+ // const newDocument = [...fragments.map((f) => f.rawSDL), documentFile.rawSDL].join('\n')
115
+
116
+ return {
117
+ filename: generatedFilePath,
118
+ documents: [...fragments, documentFile],
119
+ importStatements,
120
+ fragmentImports: [],
121
+ externalFragments: [],
122
+ } as ResolveDocumentImportResult
123
+ }
124
+
125
+ const generatedFilePath = generateFilePath(documentFile.location!)
126
+ const importStatements: string[] = []
127
+ const { externalFragments, fragmentImports } = resolveFragments(
128
+ generatedFilePath,
129
+ documentFile.document!,
130
+ )
131
+
132
+ if (
133
+ isRelayOptimizer ||
134
+ isUsingTypes(
135
+ documentFile.document!,
136
+ externalFragments.map((m) => m.name),
137
+ schemaObject,
138
+ )
139
+ ) {
140
+ const schemaTypesImportStatement = generateImportStatement({
141
+ baseDir,
142
+ importSource: resolveImportSource(schemaTypesSource),
143
+ baseOutputDir,
144
+ outputPath: generatedFilePath,
145
+ typesImport,
146
+ })
147
+ importStatements.unshift(schemaTypesImportStatement)
148
+ }
149
+
150
+ return {
151
+ filename: generatedFilePath,
152
+ documents: [documentFile],
153
+ importStatements,
154
+ fragmentImports,
155
+ externalFragments,
156
+ }
157
+ } catch (e) {
158
+ if (e instanceof Error) {
159
+ throw new DetailedError(
160
+ `Unable to validate GraphQL document!`,
161
+ `File ${documentFile.location} caused error: ${e.message || e.toString()}`,
162
+ documentFile.location,
163
+ )
164
+ } else {
165
+ throw e
166
+ }
167
+ }
168
+ })
169
+
170
+ return resDocuments.filter((result) =>
171
+ result.filename.startsWith(resolve(baseDir, baseOutputDir)),
172
+ )
173
+ }
@@ -0,0 +1,3 @@
1
+ fragment InjectableFragment on StoreConfig @injectable {
2
+ store_code
3
+ }
@@ -0,0 +1,3 @@
1
+ fragment InjectingFragment on StoreConfig @inject(into: ["InjectableFragment"]) {
2
+ base_url
3
+ }
@@ -0,0 +1,5 @@
1
+ query QueryWitInjectable {
2
+ storeConfig {
3
+ ...InjectableFragment
4
+ }
5
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,61 @@
1
+ /* eslint-disable import/no-cycle */
2
+ import { join } from 'path'
3
+ import { DocumentNode, visit, FragmentSpreadNode, FragmentDefinitionNode } from 'graphql'
4
+ import parsePath from 'parse-filepath'
5
+ import { FragmentRegistry } from './fragment-resolver'
6
+
7
+ export function defineFilepathSubfolder(baseFilePath: string, folder: string) {
8
+ const parsedPath = parsePath(baseFilePath)
9
+ return join(parsedPath.dir, folder, parsedPath.base).replace(/\\/g, '/')
10
+ }
11
+
12
+ export function appendExtensionToFilePath(baseFilePath: string, extension: string) {
13
+ const parsedPath = parsePath(baseFilePath)
14
+
15
+ return join(parsedPath.dir, parsedPath.name + extension).replace(/\\/g, '/')
16
+ }
17
+
18
+ export function extractExternalFragmentsInUse(
19
+ documentNode: DocumentNode | FragmentDefinitionNode,
20
+ fragmentNameToFile: FragmentRegistry,
21
+ result: { [fragmentName: string]: number } = {},
22
+ level = 0,
23
+ ): { [fragmentName: string]: number } {
24
+ const ignoreList: Set<string> = new Set()
25
+
26
+ // First, take all fragments definition from the current file, and mark them as ignored
27
+ visit(documentNode, {
28
+ enter: {
29
+ FragmentDefinition: (node: FragmentDefinitionNode) => {
30
+ ignoreList.add(node.name.value)
31
+ },
32
+ },
33
+ })
34
+
35
+ // Then, look for all used fragments in this document
36
+ visit(documentNode, {
37
+ enter: {
38
+ FragmentSpread: (node: FragmentSpreadNode) => {
39
+ if (!ignoreList.has(node.name.value)) {
40
+ if (
41
+ result[node.name.value] === undefined ||
42
+ (result[node.name.value] !== undefined && level < result[node.name.value])
43
+ ) {
44
+ result[node.name.value] = level
45
+
46
+ if (fragmentNameToFile[node.name.value]) {
47
+ extractExternalFragmentsInUse(
48
+ fragmentNameToFile[node.name.value].node,
49
+ fragmentNameToFile,
50
+ result,
51
+ level + 1,
52
+ )
53
+ }
54
+ }
55
+ }
56
+ },
57
+ },
58
+ })
59
+
60
+ return result
61
+ }