@graphql-tools/external-fragment-loader
Resolves missing GraphQL fragments by following direct and transitive dependencies in
package.json, loading only the external fragments the package needs.
Installation
npm install @graphql-tools/external-fragment-loaderThe resolver requires a target (consumer) package directory and one or more directories containing its dependency packages:
const options = {
packageDir: '/path/to/packages/my-app',
externalPackagesDirs: ['/path/to/packages']
}packageDir and externalPackagesDirs should be absolute paths. By default, the loader scans src
directory in the above packages for .ts, .tsx, .js, .jsx, .graphql, and .gql files.
These defaults can be changed with the options described in
options.ts.
Three API entry points
| Entry point | Returns | Use it when |
|---|---|---|
| Default export | DocumentNode (or Promise<DocumentNode> with async: true) | Configuring a Codegen or GraphQL Tools custom loader; uses the synchronous resolver by default |
ExternalFragmentLoader | Source[] with document, rawSDL, and location | Using @graphql-tools/load programmatically (can be async) |
resolveExternalFragments | File metadata, including filePath and fragment definitions | Building a file list or another custom workflow |
Each entry point has a synchronous variant where applicable. The async and sync variants are:
async: true/async: falseExternalFragmentLoader.load()/ExternalFragmentLoader.loadSync()resolveExternalFragments()/resolveExternalFragmentsSync()
1. Default loader export (for GraphQL Code Generator)
The default export returns a single merged DocumentNode. This is the simplest option for GraphQL
Code Generator because Codegen’s custom-loader path expects one DocumentNode or one Source.
This is a sample of code generation for packages/my-package. The script runs from the monorepo
root.
import { resolve } from 'node:path'
import type { CodegenConfig } from '@graphql-codegen/cli'
const packageDir = resolve('packages/my-package')
const externalPackagesDir = resolve('packages')
const config: CodegenConfig = {
schema: './schema.graphql',
documents: ['./packages/my-package/src/**/*.graphql'],
externalDocuments: [
{
// a pointer that triggers the custom loader for the consumer package
[packageDir]: {
loader: '@graphql-tools/external-fragment-loader',
packageDir,
externalPackagesDirs: [externalPackagesDir],
async: true // codegen can consume Promise<DocumentNode> directly
}
}
],
generates: {
'./packages/my-package/src/__generated__/types.ts': {
plugins: ['typescript', 'typescript-operations']
}
}
}
export default configUse at most one external-fragment pointer for each packageDir in a Codegen configuration. The
loader returns all required external fragments for that package; repeating the pointer causes those
fragments to be loaded more than once and can produce duplicate fragment definitions. Normal
document pointers can still be listed alongside the single external-fragment pointer.
The default export uses the synchronous resolver unless async: true is set. With async: true, it
returns a Promise<DocumentNode> and uses the asynchronous resolver. Use this option with Codegen
or another asynchronous loading API; omit it or set it to false when using loadDocumentsSync.
2. Class-based loader
Use ExternalFragmentLoader when calling @graphql-tools/load directly or when you need the
individual Source objects:
import { ExternalFragmentLoader } from '@graphql-tools/external-fragment-loader'
const loader = new ExternalFragmentLoader()
const sources = await loader.load('.', {
packageDir,
externalPackagesDirs: [externalPackagesDir]
})To register the class with @graphql-tools/load, pass it in the loaders option for the pointer
that represents the external-fragment document:
import { loadDocuments } from '@graphql-tools/load'
const sources = await loadDocuments(['./external-fragments.graphql'], {
loaders: [loader],
packageDir,
externalPackagesDirs: [externalPackagesDir]
})The loader also exposes a synchronous method:
const syncSources = loader.loadSync('.', options)Each returned source contains the parsed document, the matching rawSDL, and the provider file’s
location. When registering the class with @graphql-tools/load, use one external-fragment pointer
for each package being resolved. The loader does not de-duplicate separate invocations for repeated
pointers.
3. Direct resolver API
Use the resolver functions when you need the external file list or fragment metadata rather than a GraphQL Tools loader result:
import {
resolveExternalFragments,
resolveExternalFragmentsSync
} from '@graphql-tools/external-fragment-loader'
const files = await resolveExternalFragments(options)
const syncFiles = resolveExternalFragmentsSync(options)The result has this shape:
[
{
filePath: '/path/to/packages/shared/src/user-fields.graphql',
packageName: '@example/shared',
definitions: [{ name: 'UserFields', typeCondition: 'User' }]
}
]This API returns file metadata, not parsed ASTs. Passing the returned filePath values as Codegen
documents/externalDocuments works, but Codegen will read and parse those files again. Use the
default loader or the class-based loader when you want to reuse the parsed documents.
Caching
Package fragment maps, parsed sources, and package.json dependency metadata are cached between
resolver calls. Consumer package maps are not normally retained unless the consumer package was
previously loaded as a provider.
When running the loader in watch mode — for example, to run Codegen in the background while using an IDE so that the package’s generated GraphQL types are regenerated automatically whenever a file changes — consider setting the following options:
-
Set
invalidateTargetPackageCache: trueto invalidate the cached fragment map andpackage.jsondependency metadata for the target package specified bypackageDirbefore each resolution call. This prevents stale data when a package that was previously cached as a provider is later scanned as the consumer. -
Set
cacheTTL(for example, to10000for 10 seconds) so cached provider data eventually expires. This helps avoid stale data when a provider package is updated indirectly, for example by agit merge, while the watch process is still running.
Using filters
The filters are optional pre-filters: they run before the expensive dependency scanning and GraphQL parsing work. Restricting the package names and files early can be faster than parsing every source file in every dependency package.
In a benchmark of the large monorepo this loader was written for, the approximate observed time reductions were as follows (with total reduction of 68%):
fileContentFilter: 60%packageNameFilter: 4%packageDependencyFilter: 4%
Filtering files by content
Use fileContentFilter to skip source files that don’t contain GraphQL. The file content is read
once and passed to the predicate, but files returning false are not parsed or plucked for GraphQL
documents:
const options = {
packageDir,
externalPackagesDirs,
fileContentFilter: (content, filePath) => {
// Keep standalone GraphQL files and match GraphQL tags or magic comments in code files.
return (
/\.(graphql|gql)$/.test(filePath) ||
/\b(?:gql|graphql)\s*`/.test(content) ||
/\/\*\s*graphql\s*\*\//i.test(content)
)
}
}This is especially useful when packages contain many TypeScript or JavaScript files but only a small
number of them contain GraphQL tags. A file-content filter should be broad enough not to exclude
custom GraphQL tag identifiers that are configured through pluckConfig.
Filtering dependency packages by name
Use packageNameFilter to limit which package.json dependencies are considered as providers. The
filter is applied to direct and transitive dependency names before their files are scanned:
const options = {
packageDir,
externalPackagesDirs,
packageNameFilter: packageName =>
packageName.startsWith('my-company-name-') || packageName === 'shared-graphql'
}Packages rejected by this filter are not searched. If a required fragment is defined in a rejected package, the resolver reports it as unresolved.
Filtering by package dependencies
Use packageDependencyFilter to skip scanning packages that don’t use GraphQL, without affecting
dependency traversal. The filter receives the merged dependencies and devDependencies record
from each package’s package.json by default, or only dependencies when includeDevDependencies
is false. It returns boolean. Packages that return false are still traversed for their own
transitive dependencies — they are just not scanned for fragment definitions:
const options = {
packageDir,
externalPackagesDirs,
packageDependencyFilter: deps => '@apollo/client' in deps
}This is useful in large monorepos where many packages don’t contain GraphQL but sit in the dependency chain between the consumer and the packages that do. Without this filter, every reachable package gets its source files globbed and read. With the filter, only packages that pass the predicate are scanned, while non-GraphQL intermediaries are still traversed so that GraphQL packages behind them are found:
A (consumer) → B (no GraphQL) → C (has GraphQL)