diff --git a/packages/graphql-mini-transforms/README.md b/packages/graphql-mini-transforms/README.md index 38639d3..889685d 100644 --- a/packages/graphql-mini-transforms/README.md +++ b/packages/graphql-mini-transforms/README.md @@ -61,7 +61,9 @@ fragment ProductVariantId on ProductVariant { #### Options -This loader accepts a single option, `simple`. This option changes the shape of the value exported from `.graphql` files. By default, a `graphql-typed` `DocumentNode` is exported, but when `simple` is set to `true`, a `SimpleDocument` is exported instead. This representation of GraphQL documents is smaller than a full `DocumentNode`, but generally won’t work with normalized GraphQL caches. +##### simple + +This option changes the shape of the value exported from `.graphql` files. By default, a `graphql-typed` `DocumentNode` is exported, but when `simple` is set to `true`, a `SimpleDocument` is exported instead. This representation of GraphQL documents is smaller than a full `DocumentNode`, but generally won’t work with normalized GraphQL caches. ```js module.exports = { @@ -80,6 +82,27 @@ module.exports = { If this option is set to `true`, you should also use the `jest-simple` transformer for Jest, and the `--export-format simple` flag for `graphql-typescript-definitions`. +##### generateId + +This option changes the identifier value used. By default the hash of the minified GraphQL document is used as the identifier value, but when `generateId` is provided +the return value is used as the identifier value. `generateId` should be a function which takes a single parameter, the normalized GraphQL document source as a string, +and it should return a string value. + +```js +module.exports = { + module: { + rules: [ + { + test: /\.(graphql|gql)$/, + use: 'graphql-mini-transforms/webpack', + exclude: /node_modules/, + options: {generateId: normalizedSource => someHash(normalizedSource)}, + }, + ], + }, +}; +``` + ### Jest This package also provides a transformer for GraphQL files in Jest. To use the transformer, add a reference to it in your Jest configuration’s `transform` option: diff --git a/packages/graphql-mini-transforms/package.json b/packages/graphql-mini-transforms/package.json index 391d93e..3197761 100644 --- a/packages/graphql-mini-transforms/package.json +++ b/packages/graphql-mini-transforms/package.json @@ -34,6 +34,7 @@ "devDependencies": { "@types/common-tags": "^1.8.0", "@types/loader-utils": "^1.1.3", + "@types/schema-utils": "^2.4.0", "common-tags": "^1.8.0" }, "dependencies": { @@ -43,6 +44,7 @@ "fs-extra": "^9.0.0", "graphql": ">=14.5.0 <15.0.0", "graphql-typed": "^0.6.1", - "loader-utils": "^2.0.0" + "loader-utils": "^2.0.0", + "schema-utils": "^2.7.1" } } diff --git a/packages/graphql-mini-transforms/src/document.ts b/packages/graphql-mini-transforms/src/document.ts index 3c15480..7fda0c2 100644 --- a/packages/graphql-mini-transforms/src/document.ts +++ b/packages/graphql-mini-transforms/src/document.ts @@ -16,9 +16,26 @@ import {DocumentNode, SimpleDocument} from 'graphql-typed'; const IMPORT_REGEX = /^#import\s+['"]([^'"]*)['"];?[\s\n]*/gm; const DEFAULT_NAME = 'Operation'; +function defaultGenerateId(normalizedSource: string) { + // This ID is a hash of the full file contents that are part of the document, + // including other documents that are injected in, but excluding any unused + // fragments. This is useful for things like persisted queries. + return createHash('sha256') + .update(minifySource(normalizedSource)) + .digest('hex'); +} + +export interface CleanDocumentOptions { + removeUnused?: boolean; + generateId?: (normalizedSource: string) => string; +} + export function cleanDocument( document: UntypedDocumentNode, - {removeUnused = true} = {}, + { + removeUnused = true, + generateId = defaultGenerateId, + }: CleanDocumentOptions = {}, ): DocumentNode { if (removeUnused) { removeUnusedDefinitions(document); @@ -35,10 +52,7 @@ export function cleanDocument( stripLoc(definition); } - // This ID is a hash of the full file contents that are part of the document, - // including other documents that are injected in, but excluding any unused - // fragments. This is useful for things like persisted queries. - const id = createHash('sha256').update(normalizedSource).digest('hex'); + const id = generateId(print(document)); Reflect.defineProperty(normalizedDocument, 'id', { value: id, diff --git a/packages/graphql-mini-transforms/src/webpack.ts b/packages/graphql-mini-transforms/src/webpack.ts index 2bdbc90..708a899 100644 --- a/packages/graphql-mini-transforms/src/webpack.ts +++ b/packages/graphql-mini-transforms/src/webpack.ts @@ -3,13 +3,32 @@ import {dirname} from 'path'; import {loader} from 'webpack'; import {parse, DocumentNode} from 'graphql'; import {getOptions} from 'loader-utils'; +import validateOptions from 'schema-utils'; -import {cleanDocument, extractImports, toSimpleDocument} from './document'; +import { + cleanDocument, + extractImports, + toSimpleDocument, + CleanDocumentOptions, +} from './document'; interface Options { + generateId?: (normalizedSource: string) => string; simple?: boolean; } +const schema = { + type: 'object' as const, + properties: { + simple: { + type: 'boolean' as const, + }, + generateId: { + instanceof: 'Function' as const, + }, + }, +}; + export default async function graphQLLoader( this: loader.LoaderContext, source: string | Buffer, @@ -17,7 +36,9 @@ export default async function graphQLLoader( this.cacheable(); const done = this.async(); - const {simple = false} = getOptions(this) as Options; + const options = {simple: false, ...getOptions(this)} as Options; + + validateOptions(schema, options, {name: '@shopify/graphql-mini-transforms'}); if (done == null) { throw new Error( @@ -25,11 +46,16 @@ export default async function graphQLLoader( ); } + const cleanDocumentOptions = { + generateId: options.generateId, + } as CleanDocumentOptions; + try { const document = cleanDocument( await loadDocument(source, this.context, this), + cleanDocumentOptions, ); - const exported = simple ? toSimpleDocument(document) : document; + const exported = options.simple ? toSimpleDocument(document) : document; done( null, diff --git a/packages/graphql-mini-transforms/tests/webpack.test.ts b/packages/graphql-mini-transforms/tests/webpack.test.ts index 16a9a10..a5bc08e 100644 --- a/packages/graphql-mini-transforms/tests/webpack.test.ts +++ b/packages/graphql-mini-transforms/tests/webpack.test.ts @@ -70,6 +70,14 @@ describe('graphql-mini-transforms/webpack', () => { ); }); + it('has option for custom ID generate function', async () => { + const result = await extractDocumentExport( + `query Shop { shop { id } }`, + createLoaderContext({query: {generateId: () => 'foo'}}), + ); + expect(result).toHaveProperty('id', 'foo'); + }); + describe('import', () => { it('adds the resolved import as a dependency', async () => { const context = '/app/'; diff --git a/yarn.lock b/yarn.lock index 8b5ae21..100c3a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -720,6 +720,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA== +"@types/json-schema@^7.0.5": + version "7.0.6" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0" + integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw== + "@types/loader-utils@^1.1.3": version "1.1.3" resolved "https://registry.yarnpkg.com/@types/loader-utils/-/loader-utils-1.1.3.tgz#82b9163f2ead596c68a8c03e450fbd6e089df401" @@ -751,6 +756,13 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-1.19.1.tgz#33509849f8e679e4add158959fdb086440e9553f" integrity sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ== +"@types/schema-utils@^2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@types/schema-utils/-/schema-utils-2.4.0.tgz#9983012045d541dcee053e685a27c9c87c840fcd" + integrity sha512-454hrj5gz/FXcUE20ygfEiN4DxZ1sprUo0V1gqIqkNZ/CzoEzAZEll2uxMsuyz6BYjiQan4Aa65xbTemfzW9hQ== + dependencies: + schema-utils "*" + "@types/source-list-map@*": version "0.1.2" resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" @@ -909,6 +921,11 @@ add-stream@^1.0.0: resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" integrity sha1-anmQQ3ynNtXhKI25K9MmbV9csqo= +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + ajv@^6.10.0, ajv@^6.10.2, ajv@^6.5.5: version "6.12.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7" @@ -919,6 +936,16 @@ ajv@^6.10.0, ajv@^6.10.2, ajv@^6.5.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^6.12.4: + version "6.12.5" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da" + integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + ansi-escapes@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" @@ -5884,6 +5911,15 @@ saxes@^3.1.9: dependencies: xmlchars "^2.1.1" +schema-utils@*, schema-utils@^2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" + integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== + dependencies: + "@types/json-schema" "^7.0.5" + ajv "^6.12.4" + ajv-keywords "^3.5.2" + "semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.4.1, semver@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"