@graphcommerce/graphql-codegen-near-operation-file 2.102.3 → 2.103.0
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 +8 -6
- package/src/fragment-resolver.ts +191 -0
- package/src/index.ts +266 -0
- package/src/injectable.graphqls +21 -0
- package/src/injectable.ts +126 -0
- package/src/resolve-document-imports.ts +173 -0
- package/src/test/InjectableFragment.graphql +3 -0
- package/src/test/InjectingFragment.graphql +3 -0
- package/src/test/QueryWithInjectable.graphql +5 -0
- package/src/utils.ts +61 -0
package/package.json
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graphcommerce/graphql-codegen-near-operation-file",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.103.0",
|
|
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
|
-
"
|
|
16
|
+
"prepack": "yarn build",
|
|
17
|
+
"postinstall": "yarn build"
|
|
16
18
|
},
|
|
17
19
|
"prettier": "@graphcommerce/prettier-config-pwa",
|
|
18
20
|
"browserslist": [
|
|
@@ -26,9 +28,9 @@
|
|
|
26
28
|
},
|
|
27
29
|
"devDependencies": {
|
|
28
30
|
"@graphcommerce/browserslist-config-pwa": "^3.0.1",
|
|
29
|
-
"@graphcommerce/eslint-config-pwa": "^3.0.
|
|
31
|
+
"@graphcommerce/eslint-config-pwa": "^3.0.3",
|
|
30
32
|
"@graphcommerce/prettier-config-pwa": "^3.0.2",
|
|
31
|
-
"@graphcommerce/typescript-config-pwa": "^3.0
|
|
33
|
+
"@graphcommerce/typescript-config-pwa": "^3.1.0",
|
|
32
34
|
"@playwright/test": "^1.15.0"
|
|
33
35
|
},
|
|
34
36
|
"dependencies": {
|
|
@@ -39,5 +41,5 @@
|
|
|
39
41
|
"graphql": "^15.6.0",
|
|
40
42
|
"parse-filepath": "^1.0.2"
|
|
41
43
|
},
|
|
42
|
-
"gitHead": "
|
|
44
|
+
"gitHead": "1345d9b55763894d3cdedb5751895f2d3f89d1b4"
|
|
43
45
|
}
|
|
@@ -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
|
+
}
|
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
|
+
}
|