@nmarks/graphql-codegen-per-operation-file-preset 1.0.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/README.md +209 -0
- package/cjs/fragment-resolver.js +149 -0
- package/cjs/index.js +111 -0
- package/cjs/package.json +1 -0
- package/cjs/resolve-document-imports.js +58 -0
- package/cjs/utils.js +120 -0
- package/esm/fragment-resolver.js +146 -0
- package/esm/index.js +107 -0
- package/esm/resolve-document-imports.js +54 -0
- package/esm/utils.js +115 -0
- package/package.json +48 -0
- package/typings/fragment-resolver.d.cts +29 -0
- package/typings/fragment-resolver.d.ts +29 -0
- package/typings/index.d.cts +131 -0
- package/typings/index.d.ts +131 -0
- package/typings/resolve-document-imports.d.cts +48 -0
- package/typings/resolve-document-imports.d.ts +48 -0
- package/typings/utils.d.cts +23 -0
- package/typings/utils.d.ts +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# @graphql-codegen/per-operation-file-preset
|
|
2
|
+
|
|
3
|
+
Generate **one file per GraphQL operation or fragment**, named after the operation/fragment itself.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @graphql-codegen/per-operation-file-preset
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
// codegen.ts
|
|
15
|
+
import type { CodegenConfig } from '@graphql-codegen/cli';
|
|
16
|
+
|
|
17
|
+
const config: CodegenConfig = {
|
|
18
|
+
schema: 'schema.graphql',
|
|
19
|
+
documents: 'src/**/*.ts',
|
|
20
|
+
generates: {
|
|
21
|
+
'src/': {
|
|
22
|
+
preset: 'per-operation-file',
|
|
23
|
+
presetConfig: {
|
|
24
|
+
baseTypesPath: 'types.ts',
|
|
25
|
+
},
|
|
26
|
+
plugins: ['typescript-operations'],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
export default config;
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## How It Works
|
|
34
|
+
|
|
35
|
+
Unlike the `near-operation-file` preset which generates one file per source file, this preset generates **one file per operation/fragment**.
|
|
36
|
+
|
|
37
|
+
### Example
|
|
38
|
+
|
|
39
|
+
**Input:**
|
|
40
|
+
```typescript
|
|
41
|
+
// src/queries/user.ts
|
|
42
|
+
export const GetUserQuery = gql`
|
|
43
|
+
query GetUser($id: ID!) {
|
|
44
|
+
user(id: $id) {
|
|
45
|
+
id
|
|
46
|
+
name
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
export const GetUsersQuery = gql`
|
|
52
|
+
query GetUsers {
|
|
53
|
+
users {
|
|
54
|
+
id
|
|
55
|
+
name
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
`;
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Output:**
|
|
62
|
+
```
|
|
63
|
+
src/queries/__generated__/GetUser.ts # Generated types for GetUser query
|
|
64
|
+
src/queries/__generated__/GetUsers.ts # Generated types for GetUsers query
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Fragments
|
|
68
|
+
|
|
69
|
+
**Input:**
|
|
70
|
+
```typescript
|
|
71
|
+
// src/fragments/user.ts
|
|
72
|
+
export const UserFieldsFragment = gql`
|
|
73
|
+
fragment UserFields on User {
|
|
74
|
+
id
|
|
75
|
+
name
|
|
76
|
+
email
|
|
77
|
+
}
|
|
78
|
+
`;
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Output:**
|
|
82
|
+
```
|
|
83
|
+
src/fragments/__generated__/UserFields.ts
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Configuration
|
|
87
|
+
|
|
88
|
+
### `baseTypesPath` (required)
|
|
89
|
+
|
|
90
|
+
Path to your base schema types file (generated by the `typescript` plugin).
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
presetConfig: {
|
|
94
|
+
baseTypesPath: 'types.ts'
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
You can also use npm packages:
|
|
99
|
+
```typescript
|
|
100
|
+
presetConfig: {
|
|
101
|
+
baseTypesPath: '~@myapp/graphql-types'
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### `folder` (optional)
|
|
106
|
+
|
|
107
|
+
Folder name for generated files, relative to the source file location.
|
|
108
|
+
|
|
109
|
+
**Default:** `__generated__`
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
presetConfig: {
|
|
113
|
+
baseTypesPath: 'types.ts',
|
|
114
|
+
folder: 'generated' // Use 'generated' instead of '__generated__'
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### `extension` (optional)
|
|
119
|
+
|
|
120
|
+
File extension for generated files.
|
|
121
|
+
|
|
122
|
+
**Default:** `.ts`
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
presetConfig: {
|
|
126
|
+
baseTypesPath: 'types.ts',
|
|
127
|
+
extension: '.generated.ts'
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### `importTypesNamespace` (optional)
|
|
132
|
+
|
|
133
|
+
Namespace for importing base schema types.
|
|
134
|
+
|
|
135
|
+
**Default:** `Types`
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
presetConfig: {
|
|
139
|
+
baseTypesPath: 'types.ts',
|
|
140
|
+
importTypesNamespace: 'SchemaTypes'
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### `cwd` (optional)
|
|
145
|
+
|
|
146
|
+
Override the current working directory.
|
|
147
|
+
|
|
148
|
+
**Default:** `process.cwd()`
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
presetConfig: {
|
|
152
|
+
baseTypesPath: 'types.ts',
|
|
153
|
+
cwd: '/custom/path'
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Fragment Imports
|
|
158
|
+
|
|
159
|
+
The preset automatically handles fragment imports. If an operation uses a fragment from another file, it will generate the correct import statement.
|
|
160
|
+
|
|
161
|
+
**Example:**
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// src/fragments/user.ts
|
|
165
|
+
fragment UserFields on User { id name }
|
|
166
|
+
|
|
167
|
+
// src/queries/user.ts
|
|
168
|
+
query GetUser($id: ID!) {
|
|
169
|
+
user(id: $id) {
|
|
170
|
+
...UserFields
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Generated:**
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
// src/queries/__generated__/GetUser.ts
|
|
179
|
+
import { UserFieldsFragment, UserFieldsFragmentDoc } from '../../fragments/__generated__/UserFields';
|
|
180
|
+
// ... rest of generated code
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Comparison with near-operation-file
|
|
184
|
+
|
|
185
|
+
| Feature | near-operation-file | per-operation-file |
|
|
186
|
+
|---------|-------------------|-------------------|
|
|
187
|
+
| **File Strategy** | One file per source file | One file per operation/fragment |
|
|
188
|
+
| **Output Naming** | Based on source filename | Based on operation/fragment name |
|
|
189
|
+
| **Multiple Operations** | Combined in one file | Split into separate files |
|
|
190
|
+
| **Fragment Imports** | ✅ Supported | ✅ Supported |
|
|
191
|
+
| **Use Case** | Co-location with source | Migration from other tools, explicit operation files |
|
|
192
|
+
|
|
193
|
+
## When to Use This Preset
|
|
194
|
+
|
|
195
|
+
Use `per-operation-file` when:
|
|
196
|
+
- ✅ Migrating from tools that generate one file per operation
|
|
197
|
+
- ✅ You want explicit, discoverable operation files
|
|
198
|
+
- ✅ You have multiple operations per source file
|
|
199
|
+
- ✅ You want operation names to match generated file names
|
|
200
|
+
|
|
201
|
+
Use `near-operation-file` when:
|
|
202
|
+
- ✅ You want generated files to mirror your source structure
|
|
203
|
+
- ✅ You prefer one-to-one mapping between source and generated files
|
|
204
|
+
- ✅ You typically have one operation per source file
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
MIT
|
|
209
|
+
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = buildFragmentResolver;
|
|
4
|
+
const graphql_1 = require("graphql");
|
|
5
|
+
const visitor_plugin_common_1 = require("@graphql-codegen/visitor-plugin-common");
|
|
6
|
+
const utils_js_1 = require("./utils.js");
|
|
7
|
+
/**
|
|
8
|
+
* Creates fragment imports based on possible types and usage
|
|
9
|
+
*/
|
|
10
|
+
function createFragmentImports(baseVisitor, fragmentName, possibleTypes, usedTypes) {
|
|
11
|
+
const fragmentImports = [];
|
|
12
|
+
// Always include the document import
|
|
13
|
+
fragmentImports.push({
|
|
14
|
+
name: baseVisitor.getFragmentVariableName(fragmentName),
|
|
15
|
+
kind: 'document',
|
|
16
|
+
});
|
|
17
|
+
const fragmentSuffix = baseVisitor.getFragmentSuffix(fragmentName);
|
|
18
|
+
if (possibleTypes.length === 1) {
|
|
19
|
+
fragmentImports.push({
|
|
20
|
+
name: baseVisitor.convertName(fragmentName, {
|
|
21
|
+
useTypesPrefix: true,
|
|
22
|
+
suffix: fragmentSuffix,
|
|
23
|
+
}),
|
|
24
|
+
kind: 'type',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
else if (possibleTypes.length > 0) {
|
|
28
|
+
const typesToImport = usedTypes && usedTypes.length > 0 ? usedTypes : possibleTypes;
|
|
29
|
+
typesToImport.forEach(typeName => {
|
|
30
|
+
fragmentImports.push({
|
|
31
|
+
name: baseVisitor.convertName(fragmentName, {
|
|
32
|
+
useTypesPrefix: true,
|
|
33
|
+
suffix: `_${typeName}` + (fragmentSuffix.length > 0 ? `_${fragmentSuffix}` : ''),
|
|
34
|
+
}),
|
|
35
|
+
kind: 'type',
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return fragmentImports;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Used by `buildFragmentResolver` to build a mapping of fragmentNames to paths, importNames, and other useful info
|
|
43
|
+
*
|
|
44
|
+
* KEY DIFFERENCE from near-operation-file:
|
|
45
|
+
* The filePath is generated based on the fragment NAME, not the source file location
|
|
46
|
+
*/
|
|
47
|
+
function buildFragmentRegistry(baseVisitor, { folder, extension }, { documents }, schemaObject) {
|
|
48
|
+
const duplicateFragmentNames = [];
|
|
49
|
+
const registry = documents.reduce((prev, documentRecord) => {
|
|
50
|
+
const fragments = documentRecord.document.definitions.filter(d => d.kind === graphql_1.Kind.FRAGMENT_DEFINITION);
|
|
51
|
+
for (const fragment of fragments) {
|
|
52
|
+
const schemaType = schemaObject.getType(fragment.typeCondition.name.value);
|
|
53
|
+
if (!schemaType) {
|
|
54
|
+
throw new Error(`Fragment "${fragment.name.value}" is set on non-existing type "${fragment.typeCondition.name.value}"!`);
|
|
55
|
+
}
|
|
56
|
+
const fragmentName = fragment.name.value;
|
|
57
|
+
// KEY CHANGE: Generate path based on fragment name, not source file
|
|
58
|
+
const filePath = (0, utils_js_1.generateOperationFilePath)(documentRecord.location, fragmentName, folder, extension);
|
|
59
|
+
const possibleTypes = (0, visitor_plugin_common_1.getPossibleTypes)(schemaObject, schemaType);
|
|
60
|
+
const possibleTypeNames = possibleTypes.map(t => t.name);
|
|
61
|
+
const imports = createFragmentImports(baseVisitor, fragment.name.value, possibleTypeNames);
|
|
62
|
+
if (prev[fragmentName] && (0, graphql_1.print)(fragment) !== (0, graphql_1.print)(prev[fragmentName].node)) {
|
|
63
|
+
duplicateFragmentNames.push(fragmentName);
|
|
64
|
+
}
|
|
65
|
+
prev[fragmentName] = {
|
|
66
|
+
filePath,
|
|
67
|
+
imports,
|
|
68
|
+
onType: fragment.typeCondition.name.value,
|
|
69
|
+
node: fragment,
|
|
70
|
+
possibleTypes: possibleTypeNames,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return prev;
|
|
74
|
+
}, {});
|
|
75
|
+
if (duplicateFragmentNames.length) {
|
|
76
|
+
throw new Error(`Multiple fragments with the name(s) "${duplicateFragmentNames.join(', ')}" were found.`);
|
|
77
|
+
}
|
|
78
|
+
return registry;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Creates a BaseVisitor with standard configuration
|
|
82
|
+
*/
|
|
83
|
+
function createBaseVisitor(config, schemaObject) {
|
|
84
|
+
return new visitor_plugin_common_1.BaseVisitor(config, {
|
|
85
|
+
scalars: (0, visitor_plugin_common_1.buildScalarsFromConfig)(schemaObject, config),
|
|
86
|
+
dedupeOperationSuffix: (0, visitor_plugin_common_1.getConfigValue)(config.dedupeOperationSuffix, false),
|
|
87
|
+
omitOperationSuffix: (0, visitor_plugin_common_1.getConfigValue)(config.omitOperationSuffix, false),
|
|
88
|
+
fragmentVariablePrefix: (0, visitor_plugin_common_1.getConfigValue)(config.fragmentVariablePrefix, ''),
|
|
89
|
+
fragmentVariableSuffix: (0, visitor_plugin_common_1.getConfigValue)(config.fragmentVariableSuffix, 'FragmentDoc'),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Builds a fragment "resolver" that collects `externalFragments` definitions and `fragmentImportStatements`
|
|
94
|
+
*/
|
|
95
|
+
function buildFragmentResolver(collectorOptions, presetOptions, schemaObject, dedupeFragments = false) {
|
|
96
|
+
const { config } = presetOptions;
|
|
97
|
+
const baseVisitor = createBaseVisitor(config, schemaObject);
|
|
98
|
+
const fragmentRegistry = buildFragmentRegistry(baseVisitor, collectorOptions, presetOptions, schemaObject);
|
|
99
|
+
const { baseOutputDir } = presetOptions;
|
|
100
|
+
const { baseDir, typesImport } = collectorOptions;
|
|
101
|
+
function resolveFragments(generatedFilePath, documentFileContent) {
|
|
102
|
+
const { fragmentsInUse, usedFragmentTypes } = (0, utils_js_1.analyzeFragmentUsage)(documentFileContent, fragmentRegistry, schemaObject);
|
|
103
|
+
const externalFragments = [];
|
|
104
|
+
const fragmentFileImports = {};
|
|
105
|
+
for (const [fragmentName, level] of Object.entries(fragmentsInUse)) {
|
|
106
|
+
const fragmentDetails = fragmentRegistry[fragmentName];
|
|
107
|
+
if (!fragmentDetails)
|
|
108
|
+
continue;
|
|
109
|
+
// add top level references to the import object
|
|
110
|
+
// we don't check or global namespace because the calling config can do so
|
|
111
|
+
if (level === 0 ||
|
|
112
|
+
(dedupeFragments &&
|
|
113
|
+
['OperationDefinition', 'FragmentDefinition'].includes(documentFileContent.definitions[0].kind))) {
|
|
114
|
+
if (fragmentDetails.filePath !== generatedFilePath) {
|
|
115
|
+
// don't emit imports to same location
|
|
116
|
+
const usedTypesForFragment = usedFragmentTypes[fragmentName] || [];
|
|
117
|
+
const filteredImports = createFragmentImports(baseVisitor, fragmentName, fragmentDetails.possibleTypes, usedTypesForFragment);
|
|
118
|
+
if (!fragmentFileImports[fragmentDetails.filePath]) {
|
|
119
|
+
fragmentFileImports[fragmentDetails.filePath] = [];
|
|
120
|
+
}
|
|
121
|
+
fragmentFileImports[fragmentDetails.filePath].push(...filteredImports);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
externalFragments.push({
|
|
125
|
+
level,
|
|
126
|
+
isExternal: true,
|
|
127
|
+
name: fragmentName,
|
|
128
|
+
onType: fragmentDetails.onType,
|
|
129
|
+
node: fragmentDetails.node,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
externalFragments,
|
|
134
|
+
fragmentImports: Object.entries(fragmentFileImports).map(([fragmentsFilePath, identifiers]) => ({
|
|
135
|
+
baseDir,
|
|
136
|
+
baseOutputDir,
|
|
137
|
+
outputPath: generatedFilePath,
|
|
138
|
+
importSource: {
|
|
139
|
+
path: fragmentsFilePath,
|
|
140
|
+
identifiers,
|
|
141
|
+
},
|
|
142
|
+
emitLegacyCommonJSImports: presetOptions.config.emitLegacyCommonJSImports,
|
|
143
|
+
importExtension: presetOptions.config.importExtension,
|
|
144
|
+
typesImport,
|
|
145
|
+
})),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return resolveFragments;
|
|
149
|
+
}
|
package/cjs/index.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.preset = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
const graphql_1 = require("graphql");
|
|
7
|
+
const add_1 = tslib_1.__importDefault(require("@graphql-codegen/add"));
|
|
8
|
+
const visitor_plugin_common_1 = require("@graphql-codegen/visitor-plugin-common");
|
|
9
|
+
const resolve_document_imports_js_1 = require("./resolve-document-imports.js");
|
|
10
|
+
const utils_js_1 = require("./utils.js");
|
|
11
|
+
/**
|
|
12
|
+
* Extract operation and fragment names from a document
|
|
13
|
+
*/
|
|
14
|
+
function extractDefinitions(document) {
|
|
15
|
+
const definitions = [];
|
|
16
|
+
for (const def of document.definitions) {
|
|
17
|
+
if (def.kind === graphql_1.Kind.OPERATION_DEFINITION && def.name) {
|
|
18
|
+
definitions.push({ name: def.name.value, definition: def });
|
|
19
|
+
}
|
|
20
|
+
else if (def.kind === graphql_1.Kind.FRAGMENT_DEFINITION) {
|
|
21
|
+
definitions.push({ name: def.name.value, definition: def });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return definitions;
|
|
25
|
+
}
|
|
26
|
+
exports.preset = {
|
|
27
|
+
buildGeneratesSection: options => {
|
|
28
|
+
var _a;
|
|
29
|
+
const schemaObject = options.schemaAst
|
|
30
|
+
? options.schemaAst
|
|
31
|
+
: (0, graphql_1.buildASTSchema)(options.schema, options.config);
|
|
32
|
+
const baseDir = options.presetConfig.cwd || process.cwd();
|
|
33
|
+
const extension = options.presetConfig.extension || '.ts';
|
|
34
|
+
const folder = options.presetConfig.folder || '__generated__';
|
|
35
|
+
const importTypesNamespace = options.presetConfig.importTypesNamespace || 'Types';
|
|
36
|
+
const { baseTypesPath } = options.presetConfig;
|
|
37
|
+
if (!baseTypesPath) {
|
|
38
|
+
throw new Error(`Preset "per-operation-file" requires you to specify "baseTypesPath" configuration!`);
|
|
39
|
+
}
|
|
40
|
+
const shouldAbsolute = !baseTypesPath.startsWith('~');
|
|
41
|
+
const pluginMap = {
|
|
42
|
+
...options.pluginMap,
|
|
43
|
+
add: add_1.default,
|
|
44
|
+
};
|
|
45
|
+
// Resolve fragment dependencies using our adapted fragment resolver
|
|
46
|
+
// Fragment paths will be generated based on fragment names
|
|
47
|
+
const sources = (0, resolve_document_imports_js_1.resolveDocumentImports)(options, schemaObject, {
|
|
48
|
+
baseDir,
|
|
49
|
+
folder,
|
|
50
|
+
extension,
|
|
51
|
+
schemaTypesSource: {
|
|
52
|
+
path: shouldAbsolute ? (0, path_1.join)(options.baseOutputDir, baseTypesPath) : baseTypesPath,
|
|
53
|
+
namespace: importTypesNamespace,
|
|
54
|
+
},
|
|
55
|
+
typesImport: (_a = options.config.useTypeImports) !== null && _a !== void 0 ? _a : false,
|
|
56
|
+
}, (0, visitor_plugin_common_1.getConfigValue)(options.config.dedupeFragments, false));
|
|
57
|
+
const artifacts = [];
|
|
58
|
+
// Now split each source into separate files per operation/fragment
|
|
59
|
+
for (const source of sources) {
|
|
60
|
+
const definitions = extractDefinitions(source.documents[0].document);
|
|
61
|
+
for (const { name, definition } of definitions) {
|
|
62
|
+
const filename = (0, utils_js_1.generateOperationFilePath)(source.documents[0].location, name, folder, extension);
|
|
63
|
+
// Create a document with just this operation/fragment
|
|
64
|
+
const singleDefDocument = {
|
|
65
|
+
kind: graphql_1.Kind.DOCUMENT,
|
|
66
|
+
definitions: [definition],
|
|
67
|
+
};
|
|
68
|
+
const singleDefSource = {
|
|
69
|
+
rawSDL: source.documents[0].rawSDL, // TODO: might need to filter this
|
|
70
|
+
document: singleDefDocument,
|
|
71
|
+
location: source.documents[0].location,
|
|
72
|
+
};
|
|
73
|
+
// Update fragment imports to use the correct output path for this operation
|
|
74
|
+
const updatedFragmentImports = source.fragmentImports.map(fragmentImport => ({
|
|
75
|
+
...fragmentImport,
|
|
76
|
+
outputPath: filename, // Update to actual output path
|
|
77
|
+
}));
|
|
78
|
+
const plugins = [
|
|
79
|
+
...(options.config.globalNamespace
|
|
80
|
+
? []
|
|
81
|
+
: source.importStatements.map(importStatement => ({
|
|
82
|
+
add: { content: importStatement },
|
|
83
|
+
}))),
|
|
84
|
+
...options.plugins,
|
|
85
|
+
];
|
|
86
|
+
const config = {
|
|
87
|
+
...options.config,
|
|
88
|
+
exportFragmentSpreadSubTypes: true,
|
|
89
|
+
namespacedImportName: importTypesNamespace,
|
|
90
|
+
externalFragments: source.externalFragments,
|
|
91
|
+
fragmentImports: updatedFragmentImports,
|
|
92
|
+
};
|
|
93
|
+
artifacts.push({
|
|
94
|
+
...options,
|
|
95
|
+
filename,
|
|
96
|
+
documents: [singleDefSource],
|
|
97
|
+
plugins,
|
|
98
|
+
pluginMap,
|
|
99
|
+
config,
|
|
100
|
+
schema: options.schema,
|
|
101
|
+
schemaAst: schemaObject,
|
|
102
|
+
skipDocumentsValidation: typeof options.config.skipDocumentsValidation === 'undefined'
|
|
103
|
+
? { skipDuplicateValidation: true }
|
|
104
|
+
: options.config.skipDocumentsValidation,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return artifacts;
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
exports.default = exports.preset;
|
package/cjs/package.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveDocumentImports = resolveDocumentImports;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const plugin_helpers_1 = require("@graphql-codegen/plugin-helpers");
|
|
6
|
+
const visitor_plugin_common_1 = require("@graphql-codegen/visitor-plugin-common");
|
|
7
|
+
const fragment_resolver_js_1 = tslib_1.__importDefault(require("./fragment-resolver.js"));
|
|
8
|
+
/**
|
|
9
|
+
* Transform the preset's provided documents into single-file generator sources, while resolving fragment and user-defined imports
|
|
10
|
+
*
|
|
11
|
+
* Resolves user provided imports and fragment imports using the `DocumentImportResolverOptions`.
|
|
12
|
+
* Does not define specific plugins, but rather returns a string[] of `importStatements` for the calling plugin to make use of
|
|
13
|
+
*/
|
|
14
|
+
function resolveDocumentImports(presetOptions, schemaObject, importResolverOptions, dedupeFragments = false) {
|
|
15
|
+
const resolveFragments = (0, fragment_resolver_js_1.default)(importResolverOptions, presetOptions, schemaObject, dedupeFragments);
|
|
16
|
+
const { baseOutputDir, documents } = presetOptions;
|
|
17
|
+
const { schemaTypesSource, baseDir, typesImport } = importResolverOptions;
|
|
18
|
+
return documents.map(documentFile => {
|
|
19
|
+
try {
|
|
20
|
+
// NOTE: We pass a placeholder filename here since we'll generate proper filenames per-operation later
|
|
21
|
+
// The important part is that fragment resolution will use the correct paths from the registry
|
|
22
|
+
const placeholderFilePath = documentFile.location;
|
|
23
|
+
const importStatements = [];
|
|
24
|
+
const { externalFragments, fragmentImports } = resolveFragments(placeholderFilePath, documentFile.document);
|
|
25
|
+
const externalFragmentsInjectedDocument = {
|
|
26
|
+
...documentFile.document,
|
|
27
|
+
definitions: [
|
|
28
|
+
...documentFile.document.definitions,
|
|
29
|
+
...externalFragments.map(fragment => fragment.node),
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
if ((0, plugin_helpers_1.isUsingTypes)(externalFragmentsInjectedDocument, [], schemaObject)) {
|
|
33
|
+
const schemaTypesImportStatement = (0, visitor_plugin_common_1.generateImportStatement)({
|
|
34
|
+
baseDir,
|
|
35
|
+
emitLegacyCommonJSImports: presetOptions.config.emitLegacyCommonJSImports,
|
|
36
|
+
importExtension: presetOptions.config.importExtension,
|
|
37
|
+
importSource: (0, visitor_plugin_common_1.resolveImportSource)(schemaTypesSource),
|
|
38
|
+
baseOutputDir,
|
|
39
|
+
outputPath: placeholderFilePath,
|
|
40
|
+
typesImport,
|
|
41
|
+
});
|
|
42
|
+
importStatements.unshift(schemaTypesImportStatement);
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
filename: placeholderFilePath,
|
|
46
|
+
documents: [documentFile],
|
|
47
|
+
importStatements,
|
|
48
|
+
fragmentImports,
|
|
49
|
+
externalFragments,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
throw new Error(`Unable to validate GraphQL document! \n
|
|
54
|
+
File ${documentFile.location} caused error:
|
|
55
|
+
${e.message || e.toString()}`);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
package/cjs/utils.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateOperationFilePath = generateOperationFilePath;
|
|
4
|
+
exports.analyzeFragmentUsage = analyzeFragmentUsage;
|
|
5
|
+
exports.extractExternalFragmentsInUse = extractExternalFragmentsInUse;
|
|
6
|
+
const path_1 = require("path");
|
|
7
|
+
const graphql_1 = require("graphql");
|
|
8
|
+
const visitor_plugin_common_1 = require("@graphql-codegen/visitor-plugin-common");
|
|
9
|
+
/**
|
|
10
|
+
* Generate output file path for a specific operation/fragment
|
|
11
|
+
*/
|
|
12
|
+
function generateOperationFilePath(sourceLocation, operationName, folder, extension) {
|
|
13
|
+
const dir = (0, path_1.dirname)(sourceLocation);
|
|
14
|
+
return (0, path_1.join)(dir, folder, `${operationName}${extension}`).replace(/\\/g, '/');
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Analyzes fragment usage in a GraphQL document.
|
|
18
|
+
* Returns information about which fragments are used and which specific types they're used with.
|
|
19
|
+
*/
|
|
20
|
+
function analyzeFragmentUsage(documentNode, fragmentRegistry, schema) {
|
|
21
|
+
const localFragments = getLocalFragments(documentNode);
|
|
22
|
+
const fragmentsInUse = extractExternalFragmentsInUse(documentNode, fragmentRegistry, localFragments);
|
|
23
|
+
const usedFragmentTypes = analyzeFragmentTypeUsage(documentNode, fragmentRegistry, schema, localFragments, fragmentsInUse);
|
|
24
|
+
return { fragmentsInUse, usedFragmentTypes };
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Get all fragment definitions that are local to this document
|
|
28
|
+
*/
|
|
29
|
+
function getLocalFragments(documentNode) {
|
|
30
|
+
const localFragments = new Set();
|
|
31
|
+
(0, graphql_1.visit)(documentNode, {
|
|
32
|
+
FragmentDefinition: node => {
|
|
33
|
+
localFragments.add(node.name.value);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
return localFragments;
|
|
37
|
+
}
|
|
38
|
+
function extractExternalFragmentsInUse(documentNode, fragmentNameToFile, localFragment, result = {}, level = 0) {
|
|
39
|
+
// Then, look for all used fragments in this document
|
|
40
|
+
(0, graphql_1.visit)(documentNode, {
|
|
41
|
+
FragmentSpread: node => {
|
|
42
|
+
if (!localFragment.has(node.name.value) &&
|
|
43
|
+
(result[node.name.value] === undefined || level < result[node.name.value])) {
|
|
44
|
+
result[node.name.value] = level;
|
|
45
|
+
if (fragmentNameToFile[node.name.value]) {
|
|
46
|
+
extractExternalFragmentsInUse(fragmentNameToFile[node.name.value].node, fragmentNameToFile, localFragment, result, level + 1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Analyze which specific types each fragment is used with (for polymorphic fragments)
|
|
55
|
+
*/
|
|
56
|
+
function analyzeFragmentTypeUsage(documentNode, fragmentRegistry, schema, localFragments, fragmentsInUse) {
|
|
57
|
+
const usedFragmentTypes = {};
|
|
58
|
+
const typeInfo = new graphql_1.TypeInfo(schema);
|
|
59
|
+
(0, graphql_1.visit)(documentNode, (0, graphql_1.visitWithTypeInfo)(typeInfo, {
|
|
60
|
+
Field: (node) => {
|
|
61
|
+
if (!node.selectionSet)
|
|
62
|
+
return;
|
|
63
|
+
const fieldType = typeInfo.getType();
|
|
64
|
+
if (!fieldType)
|
|
65
|
+
return;
|
|
66
|
+
const baseType = getBaseType(fieldType);
|
|
67
|
+
if ((0, graphql_1.isObjectType)(baseType) || (0, graphql_1.isInterfaceType)(baseType) || (0, graphql_1.isUnionType)(baseType)) {
|
|
68
|
+
analyzeSelectionSetTypeContext(node.selectionSet, baseType.name, usedFragmentTypes, fragmentRegistry, schema, localFragments);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
}));
|
|
72
|
+
const result = {};
|
|
73
|
+
// Fill in missing types for multi-type fragments
|
|
74
|
+
for (const fragmentName in fragmentsInUse) {
|
|
75
|
+
const fragment = fragmentRegistry[fragmentName];
|
|
76
|
+
if (!fragment || fragment.possibleTypes.length <= 1)
|
|
77
|
+
continue;
|
|
78
|
+
const usedTypes = usedFragmentTypes[fragmentName];
|
|
79
|
+
result[fragmentName] = (usedTypes === null || usedTypes === void 0 ? void 0 : usedTypes.size) > 0 ? Array.from(usedTypes) : fragment.possibleTypes;
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Analyze fragment usage within a specific selection set and type context
|
|
85
|
+
*/
|
|
86
|
+
function analyzeSelectionSetTypeContext(selectionSet, currentTypeName, usedFragmentTypes, fragmentRegistry, schema, localFragments) {
|
|
87
|
+
var _a, _b;
|
|
88
|
+
var _c;
|
|
89
|
+
const { spreads, inlines } = (0, visitor_plugin_common_1.separateSelectionSet)(selectionSet.selections);
|
|
90
|
+
// Process fragment spreads in this type context
|
|
91
|
+
for (const spread of spreads) {
|
|
92
|
+
if (localFragments.has(spread.name.value))
|
|
93
|
+
continue;
|
|
94
|
+
const fragment = fragmentRegistry[spread.name.value];
|
|
95
|
+
if (!fragment || fragment.possibleTypes.length <= 1)
|
|
96
|
+
continue;
|
|
97
|
+
const currentType = schema.getType(currentTypeName);
|
|
98
|
+
if (!currentType)
|
|
99
|
+
continue;
|
|
100
|
+
const possibleTypes = (0, visitor_plugin_common_1.getPossibleTypes)(schema, currentType).map(t => t.name);
|
|
101
|
+
const matchingTypes = possibleTypes.filter(type => fragment.possibleTypes.includes(type));
|
|
102
|
+
if (matchingTypes.length > 0) {
|
|
103
|
+
const typeSet = ((_a = usedFragmentTypes[_c = spread.name.value]) !== null && _a !== void 0 ? _a : (usedFragmentTypes[_c] = new Set()));
|
|
104
|
+
matchingTypes.forEach(type => typeSet.add(type));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Process inline fragments
|
|
108
|
+
for (const inline of inlines) {
|
|
109
|
+
if (((_b = inline.typeCondition) === null || _b === void 0 ? void 0 : _b.name.value) && inline.selectionSet) {
|
|
110
|
+
analyzeSelectionSetTypeContext(inline.selectionSet, inline.typeCondition.name.value, usedFragmentTypes, fragmentRegistry, schema, localFragments);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function getBaseType(type) {
|
|
115
|
+
let baseType = type;
|
|
116
|
+
while ('ofType' in baseType && baseType.ofType) {
|
|
117
|
+
baseType = baseType.ofType;
|
|
118
|
+
}
|
|
119
|
+
return baseType;
|
|
120
|
+
}
|