diff --git a/packages/plugins/other/fragment-matcher/src/index.ts b/packages/plugins/other/fragment-matcher/src/index.ts index a4b9777bd86..6664bfbaa92 100644 --- a/packages/plugins/other/fragment-matcher/src/index.ts +++ b/packages/plugins/other/fragment-matcher/src/index.ts @@ -109,6 +109,11 @@ export interface FragmentMatcherConfig { */ useExplicitTyping?: boolean; federation?: boolean; + /** + * @description When enabled sorts the fragment types lexicographically. This is useful for deterministic output. + * @default false + */ + deterministic?: boolean; } const extensions = { @@ -128,6 +133,7 @@ export const plugin: PluginFunction = async ( federation: false, apolloClientVersion: 3, useExplicitTyping: false, + deterministic: false, ...pluginConfig, }; @@ -157,9 +163,22 @@ export const plugin: PluginFunction = async ( throw new Error(`Plugin "fragment-matcher" couldn't introspect the schema`); } - const filterUnionAndInterfaceTypes = type => type.kind === 'UNION' || type.kind === 'INTERFACE'; + const sortStringsLexicographically = (a: string, b: string) => { + if (!config.deterministic) { + return 0; + } + return a.localeCompare(b); + }; + + const unionAndInterfaceTypes = introspection.data.__schema.types + .filter(type => type.kind === 'UNION' || type.kind === 'INTERFACE') + .sort((a, b) => sortStringsLexicographically(a.name, b.name)); + const createPossibleTypesCollection = (acc, type) => { - return { ...acc, [type.name]: type.possibleTypes.map(possibleType => possibleType.name) }; + return { + ...acc, + [type.name]: type.possibleTypes.map(possibleType => possibleType.name).sort(sortStringsLexicographically), + }; }; const filteredData: IntrospectionResultData | PossibleTypesResultData = @@ -167,13 +186,11 @@ export const plugin: PluginFunction = async ( ? { __schema: { ...introspection.data.__schema, - types: introspection.data.__schema.types.filter(type => type.kind === 'UNION' || type.kind === 'INTERFACE'), + types: unionAndInterfaceTypes, }, } : { - possibleTypes: introspection.data.__schema.types - .filter(filterUnionAndInterfaceTypes) - .reduce(createPossibleTypesCollection, {}), + possibleTypes: unionAndInterfaceTypes.reduce(createPossibleTypesCollection, {}), }; const content = JSON.stringify(filteredData, null, 2); diff --git a/packages/plugins/other/fragment-matcher/tests/plugin.spec.ts b/packages/plugins/other/fragment-matcher/tests/plugin.spec.ts index 0a615954a2f..07c6e8f8bb8 100644 --- a/packages/plugins/other/fragment-matcher/tests/plugin.spec.ts +++ b/packages/plugins/other/fragment-matcher/tests/plugin.spec.ts @@ -444,4 +444,72 @@ describe('Fragment Matcher Plugin', () => { expect(content).toEqual(introspection); }); + it('should create the result deterministically when configured to', async () => { + const complexSchema = buildASTSchema(gql` + type Droid { + model: String + } + + type Character { + name: String + } + + type Jedi { + side: String + } + + union People = Jedi | Droid | Character + union People2 = Droid | Jedi | Character + + type Query { + allPeople: [People] + } + `); + + const reorderedComplexSchema = buildASTSchema(gql` + type Droid { + model: String + } + + type Character { + name: String + } + + type Jedi { + side: String + } + + union People2 = Droid | Jedi | Character + union People = Jedi | Droid | Character + + type Query { + allPeople: [People] + } + `); + + const contentA = await plugin( + complexSchema, + [], + { + apolloClientVersion: 2, + deterministic: true, + }, + { + outputFile: 'foo.json', + } + ); + const contentB = await plugin( + reorderedComplexSchema, + [], + { + apolloClientVersion: 2, + deterministic: true, + }, + { + outputFile: 'foo.json', + } + ); + + expect(contentA).toEqual(contentB); + }); });