@graphql-tools/external-fragment-loader 0.0.0-stage → 0.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 CHANGED
@@ -1,3 +1,257 @@
1
- # Temporary Holding Version
1
+ # @graphql-tools/external-fragment-loader
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Resolves GraphQL fragments referenced by a package from its transitive package dependencies. The
4
+ loader scans the configured dependency packages, parses GraphQL from `.graphql` files and supported
5
+ code files, and returns only the external fragments required by the consumer package.
6
+
7
+ ## Installation
8
+
9
+ ```sh
10
+ npm install @graphql-tools/external-fragment-loader graphql
11
+ ```
12
+
13
+ The resolver requires a package directory and one or more directories containing its dependency
14
+ packages:
15
+
16
+ ```ts
17
+ const options = {
18
+ packageDir: '/path/to/packages/my-app',
19
+ externalPackagesDirs: ['/path/to/packages']
20
+ }
21
+ ```
22
+
23
+ `packageDir` and `externalPackagesDirs` should be absolute paths. By default, the loader scans `src`
24
+ for `.ts`, `.tsx`, `.js`, `.jsx`, `.graphql`, and `.gql` files. These defaults can be changed with
25
+ the options described in [`options.ts`](./src/options.ts).
26
+
27
+ ## Three API entry points
28
+
29
+ | Entry point | Returns | Use it when |
30
+ | -------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
31
+ | Default export | `DocumentNode` (or `Promise<DocumentNode>` with `async: true`) | Configuring a Codegen or GraphQL Tools custom loader; uses the synchronous resolver by default |
32
+ | `ExternalFragmentLoader` | `Source[]` with `document`, `rawSDL`, and `location` | Using `@graphql-tools/load` programmatically (can be async) |
33
+ | `resolveExternalFragments` | File metadata, including `filePath` and fragment definitions | Building a file list or another custom workflow |
34
+
35
+ Each entry point has a synchronous variant where applicable. The async and sync variants are:
36
+
37
+ - `ExternalFragmentLoader.load()` / `loadSync()`
38
+ - `resolveExternalFragments()` / `resolveExternalFragmentsSync()`
39
+
40
+ ### 1. Default loader export
41
+
42
+ The default export returns a single merged `DocumentNode`. This is the simplest option for GraphQL
43
+ Code Generator because Codegen's custom-loader path expects one `DocumentNode` or one `Source`.
44
+
45
+ This is a sample of code generation for `packages/my-package`. The script runs from the monorepo
46
+ root.
47
+
48
+ ```ts
49
+ import { resolve } from 'node:path'
50
+ import type { CodegenConfig } from '@graphql-codegen/cli'
51
+
52
+ const packageDir = resolve('packages/my-package')
53
+ const externalPackagesDir = resolve('packages')
54
+
55
+ const config: CodegenConfig = {
56
+ schema: './schema.graphql',
57
+ documents: [
58
+ './packages/my-package/src/**/*.graphql',
59
+ {
60
+ [packageDir]: {
61
+ // a pointer that triggers the custom loader for the consumer package
62
+ loader: '@graphql-tools/external-fragment-loader',
63
+ packageDir,
64
+ externalPackagesDirs: [externalPackagesDir],
65
+ async: true // codegen can consume Promise<DocumentNode> directly
66
+ }
67
+ }
68
+ ],
69
+ generates: {
70
+ './packages/my-package/src/__generated__/types.ts': {
71
+ plugins: ['typescript', 'typescript-operations']
72
+ }
73
+ }
74
+ }
75
+
76
+ export default config
77
+ ```
78
+
79
+ Use at most one external-fragment pointer for each `packageDir` in a Codegen configuration. The
80
+ loader returns all required external fragments for that package; repeating the pointer causes those
81
+ fragments to be loaded more than once and can produce duplicate fragment definitions. Normal
82
+ document pointers can still be listed alongside the single external-fragment pointer.
83
+
84
+ The default export uses the synchronous resolver unless `async: true` is set. With `async: true`, it
85
+ returns a `Promise<DocumentNode>` and uses the asynchronous resolver. Use this option with Codegen
86
+ or another asynchronous loading API; omit it or set it to `false` when using `loadDocumentsSync`.
87
+
88
+ ### 2. Class-based loader
89
+
90
+ Use `ExternalFragmentLoader` when calling `@graphql-tools/load` directly or when you need the
91
+ individual `Source` objects:
92
+
93
+ ```ts
94
+ import { ExternalFragmentLoader } from '@graphql-tools/external-fragment-loader'
95
+
96
+ const loader = new ExternalFragmentLoader()
97
+
98
+ const sources = await loader.load('.', {
99
+ packageDir,
100
+ externalPackagesDirs: [externalPackagesDir]
101
+ })
102
+ ```
103
+
104
+ To register the class with `@graphql-tools/load`, pass it in the `loaders` option for the pointer
105
+ that represents the external-fragment document:
106
+
107
+ ```ts
108
+ import { loadDocuments } from '@graphql-tools/load'
109
+
110
+ const sources = await loadDocuments(['./external-fragments.graphql'], {
111
+ loaders: [loader],
112
+ packageDir,
113
+ externalPackagesDirs: [externalPackagesDir]
114
+ })
115
+ ```
116
+
117
+ The loader also exposes a **synchronous** method:
118
+
119
+ ```ts
120
+ const syncSources = loader.loadSync('.', options)
121
+ ```
122
+
123
+ Each returned source contains the parsed `document`, the matching `rawSDL`, and the provider file's
124
+ `location`. When registering the class with `@graphql-tools/load`, use one external-fragment pointer
125
+ for each package being resolved. The loader does not de-duplicate separate invocations for repeated
126
+ pointers.
127
+
128
+ ### 3. Direct resolver API
129
+
130
+ Use the resolver functions when you need the external file list or fragment metadata rather than a
131
+ GraphQL Tools loader result:
132
+
133
+ ```ts
134
+ import {
135
+ resolveExternalFragments,
136
+ resolveExternalFragmentsSync
137
+ } from '@graphql-tools/external-fragment-loader'
138
+
139
+ const files = await resolveExternalFragments(options)
140
+ const syncFiles = resolveExternalFragmentsSync(options)
141
+ ```
142
+
143
+ The result has this shape:
144
+
145
+ ```text
146
+ [
147
+ {
148
+ filePath: '/path/to/packages/shared/src/user-fields.graphql',
149
+ packageName: '@example/shared',
150
+ definitions: [{ name: 'UserFields', typeCondition: 'User' }]
151
+ }
152
+ ]
153
+ ```
154
+
155
+ This API returns file metadata, not parsed ASTs. Passing the returned `filePath` values as Codegen
156
+ `documents` works, but Codegen will read and parse those files again. Use the default loader or the
157
+ class-based loader when you want to reuse the parsed documents.
158
+
159
+ ## Caching
160
+
161
+ Package fragment maps, parsed sources, and `package.json` dependency metadata are cached between
162
+ resolver calls. Consumer package maps are not normally retained unless the consumer package was
163
+ previously loaded as a provider.
164
+
165
+ When running the loader in **watch mode** — for example, to run Codegen in the background while
166
+ using an IDE so that the package's generated GraphQL types are regenerated automatically whenever a
167
+ file changes — consider setting the following options:
168
+
169
+ - Set `invalidateTargetPackageCache: true` to invalidate the cached fragment map and `package.json`
170
+ dependency metadata for the target package specified by `packageDir` before each resolution call.
171
+ This prevents stale data when a package that was previously cached as a provider is later scanned
172
+ as the consumer.
173
+
174
+ - Set `cacheTTL` (for example, to `10000` for 10 seconds) so cached provider data eventually
175
+ expires. This helps avoid stale data when a provider package is updated indirectly, for example by
176
+ a `git merge`, while the watch process is still running.
177
+
178
+ ## Using filters
179
+
180
+ The filters are optional pre-filters: they run before the expensive dependency scanning and GraphQL
181
+ parsing work. Restricting the package names and files early can be faster than parsing every source
182
+ file in every dependency package.
183
+
184
+ In a benchmark of the large monorepo this loader was written for, the approximate observed time
185
+ reductions were as follows (with total reduction of 68%):
186
+
187
+ - `fileContentFilter`: 60%
188
+ - `packageNameFilter`: 4%
189
+ - `packageDependencyFilter`: 4%
190
+
191
+ ### Filtering files by content
192
+
193
+ Use `fileContentFilter` to skip source files that don't contain GraphQL. The file content is read
194
+ once and passed to the predicate, but files returning `false` are not parsed or plucked for GraphQL
195
+ documents:
196
+
197
+ ```ts
198
+ const options = {
199
+ packageDir,
200
+ externalPackagesDirs,
201
+ fileContentFilter: (content, filePath) => {
202
+ // Keep standalone GraphQL files and match GraphQL tags or magic comments in code files.
203
+ return (
204
+ /\.(graphql|gql)$/.test(filePath) ||
205
+ /\b(?:gql|graphql)\s*`/.test(content) ||
206
+ /\/\*\s*graphql\s*\*\//i.test(content)
207
+ )
208
+ }
209
+ }
210
+ ```
211
+
212
+ This is especially useful when packages contain many TypeScript or JavaScript files but only a small
213
+ number of them contain GraphQL tags. A file-content filter should be broad enough not to exclude
214
+ custom GraphQL tag identifiers that are configured through `pluckConfig`.
215
+
216
+ ### Filtering dependency packages by name
217
+
218
+ Use `packageNameFilter` to limit which `package.json` dependencies are considered as providers. The
219
+ filter is applied to direct and transitive dependency names before their files are scanned:
220
+
221
+ ```ts
222
+ const options = {
223
+ packageDir,
224
+ externalPackagesDirs,
225
+ packageNameFilter: packageName =>
226
+ packageName.startsWith('my-company-name-') || packageName === 'shared-graphql'
227
+ }
228
+ ```
229
+
230
+ Packages rejected by this filter are not searched. If a required fragment is defined in a rejected
231
+ package, the resolver reports it as unresolved.
232
+
233
+ ### Filtering by package dependencies
234
+
235
+ Use `packageDependencyFilter` to skip scanning packages that don't use GraphQL, without affecting
236
+ dependency traversal. The filter receives the merged `dependencies` and `devDependencies` record
237
+ from each package's `package.json` by default, or only `dependencies` when `includeDevDependencies`
238
+ is `false`. It returns `boolean`. Packages that return `false` are still traversed for their own
239
+ transitive dependencies — they are just not scanned for fragment definitions:
240
+
241
+ ```ts
242
+ const options = {
243
+ packageDir,
244
+ externalPackagesDirs,
245
+ packageDependencyFilter: deps => '@apollo/client' in deps
246
+ }
247
+ ```
248
+
249
+ This is useful in large monorepos where many packages don't contain GraphQL but sit in the
250
+ dependency chain between the consumer and the packages that do. Without this filter, every reachable
251
+ package gets its source files globbed and read. With the filter, only packages that pass the
252
+ predicate are scanned, while non-GraphQL intermediaries are still traversed so that GraphQL packages
253
+ behind them are found:
254
+
255
+ ```text
256
+ A (consumer) → B (no GraphQL) → C (has GraphQL)
257
+ ```
package/cjs/index.js ADDED
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.clearCache = exports.resolveExternalFragmentsSync = exports.resolveExternalFragments = exports.ExternalFragmentLoader = exports.default = void 0;
4
+ const tslib_1 = require("tslib");
5
+ var loader_js_1 = require("./loader.js");
6
+ Object.defineProperty(exports, "default", { enumerable: true, get: function () { return tslib_1.__importDefault(loader_js_1).default; } });
7
+ var loader_js_2 = require("./loader.js");
8
+ Object.defineProperty(exports, "ExternalFragmentLoader", { enumerable: true, get: function () { return loader_js_2.ExternalFragmentLoader; } });
9
+ var resolve_js_1 = require("./resolve.js");
10
+ Object.defineProperty(exports, "resolveExternalFragments", { enumerable: true, get: function () { return resolve_js_1.resolveExternalFragments; } });
11
+ Object.defineProperty(exports, "resolveExternalFragmentsSync", { enumerable: true, get: function () { return resolve_js_1.resolveExternalFragmentsSync; } });
12
+ Object.defineProperty(exports, "clearCache", { enumerable: true, get: function () { return resolve_js_1.clearCache; } });
package/cjs/loader.js ADDED
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ExternalFragmentLoader = void 0;
4
+ exports.default = externalFragmentLoader;
5
+ const graphql_1 = require("graphql");
6
+ const resolve_js_1 = require("./resolve.js");
7
+ function mergeResolvedDocuments(resolvedFiles) {
8
+ return (0, graphql_1.concatAST)(resolvedFiles.flatMap(file => file.sources.map(source => source.document)));
9
+ }
10
+ function externalFragmentLoader(_pointer, options) {
11
+ if (options.async === true) {
12
+ return (0, resolve_js_1.resolveExternalFragmentsWithSources)(options).then(mergeResolvedDocuments);
13
+ }
14
+ return mergeResolvedDocuments((0, resolve_js_1.resolveExternalFragmentsSyncWithSources)(options));
15
+ }
16
+ class ExternalFragmentLoader {
17
+ async load(_pointer, options) {
18
+ const resolvedFiles = await (0, resolve_js_1.resolveExternalFragmentsWithSources)(options);
19
+ return resolvedFiles.flatMap(file => file.sources.map(source => ({
20
+ location: file.filePath,
21
+ ...source,
22
+ })));
23
+ }
24
+ loadSync(_pointer, options) {
25
+ const resolvedFiles = (0, resolve_js_1.resolveExternalFragmentsSyncWithSources)(options);
26
+ return resolvedFiles.flatMap(file => file.sources.map(source => ({
27
+ location: file.filePath,
28
+ ...source,
29
+ })));
30
+ }
31
+ }
32
+ exports.ExternalFragmentLoader = ExternalFragmentLoader;
package/cjs/options.js ADDED
File without changes
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}