diff --git a/.changeset/thirty-parents-walk.md b/.changeset/thirty-parents-walk.md new file mode 100644 index 00000000000..a9b44dfba4a --- /dev/null +++ b/.changeset/thirty-parents-walk.md @@ -0,0 +1,6 @@ +--- +'@graphql-tools/graphql-tag-pluck': patch +'@graphql-tools/code-file-loader': patch +--- + +add `.gts` and `.gjs` file support diff --git a/packages/graphql-tag-pluck/package.json b/packages/graphql-tag-pluck/package.json index 924c66e26f2..289c9790bb2 100644 --- a/packages/graphql-tag-pluck/package.json +++ b/packages/graphql-tag-pluck/package.json @@ -66,6 +66,7 @@ "@types/babel__traverse": "7.20.6", "@vue/compiler-sfc": "3.5.6", "astrojs-compiler-sync": "^1.0.0", + "content-tag": "^2.0.1", "svelte": "4.2.19", "svelte2tsx": "0.7.19" }, diff --git a/packages/graphql-tag-pluck/src/config.ts b/packages/graphql-tag-pluck/src/config.ts index 5091aae312b..f2ecf0cb6d1 100644 --- a/packages/graphql-tag-pluck/src/config.ts +++ b/packages/graphql-tag-pluck/src/config.ts @@ -80,6 +80,15 @@ export default function generateConfig( case '.astro': plugins.push('typescript', 'jsx'); break; + case '.gts': + plugins.push('typescript'); + break; + case '.gjs': + // .gjs files need to be parsed as TypeScript because Ember relies on decorators, which are handled by TypeScript. + // without this, it throws a SyntaxError: Unexpected token, expected "{" + // when native decorators are supported, we should remove this + plugins.push('typescript'); + break; default: plugins.push('jsx', ...dynamicFlowPlugins); break; diff --git a/packages/graphql-tag-pluck/src/index.ts b/packages/graphql-tag-pluck/src/index.ts index 4906097a2d7..99c85a90097 100644 --- a/packages/graphql-tag-pluck/src/index.ts +++ b/packages/graphql-tag-pluck/src/index.ts @@ -147,6 +147,8 @@ const supportedExtensions = [ '.vue', '.svelte', '.astro', + '.gts', + '.gjs', ]; // tslint:disable-next-line: no-implicit-dependencies @@ -196,6 +198,11 @@ function parseWithAstroSync( return fileInTsx.code; } +function transformGlimmerFile(glimmerSyntax: typeof import('content-tag'), fileData: string) { + const processor = new glimmerSyntax.Preprocessor(); + return processor.process(fileData); +} + /** * Asynchronously plucks GraphQL template literals from a single file. * @@ -224,6 +231,8 @@ export const gqlPluckFromCodeString = async ( code = await pluckSvelteFileScript(code); } else if (fileExt === '.astro') { code = await pluckAstroFileScript(code); + } else if (fileExt === '.gts' || fileExt === '.gjs') { + code = await pluckGlimmerFileScript(code); } const sources = parseCode({ code, filePath, options }).map( @@ -238,7 +247,7 @@ export const gqlPluckFromCodeString = async ( /** * Synchronously plucks GraphQL template literals from a single file * - * Supported file extensions include: `.js`, `.mjs`, `.cjs`, `.jsx`, `.ts`, `.mjs`, `.cjs`, `.tsx`, `.flow`, `.flow.js`, `.flow.jsx`, `.vue`, `.svelte`, `.astro` + * Supported file extensions include: `.js`, `.mjs`, `.cjs`, `.jsx`, `.ts`, `.mjs`, `.cjs`, `.tsx`, `.flow`, `.flow.js`, `.flow.jsx`, `.vue`, `.svelte`, `.astro`, `.gts`, `.gjs` * * @param filePath Path to the file containing the code. Required to detect the file type * @param code The contents of the file being parsed. @@ -263,6 +272,8 @@ export const gqlPluckFromCodeStringSync = ( code = pluckSvelteFileScriptSync(code); } else if (fileExt === '.astro') { code = pluckAstroFileScriptSync(code); + } else if (fileExt === '.gts' || fileExt === '.gjs') { + code = pluckGlimmerFileScriptSync(code); } const sources = parseCode({ code, filePath, options }).map( @@ -359,6 +370,21 @@ const MissingAstroCompilerError = new Error( `), ); +const MissingGlimmerCompilerError = new Error( + freeText(` + GraphQL template literals cannot be plucked from a Glimmer template code without having the "content-tag" package installed. + Please install it and try again. + + Via NPM: + + $ npm install content-tag + + Via Yarn: + + $ yarn add content-tag + `), +); + async function loadVueCompilerAsync() { try { // eslint-disable-next-line import/no-extraneous-dependencies @@ -445,3 +471,25 @@ function pluckAstroFileScriptSync(fileData: string) { return parseWithAstroSync(astroCompiler, fileData); } + +async function pluckGlimmerFileScript(fileData: string) { + let contentTag: typeof import('content-tag'); + try { + contentTag = await import('content-tag'); + } catch { + throw MissingGlimmerCompilerError; + } + + return transformGlimmerFile(contentTag, fileData); +} + +function pluckGlimmerFileScriptSync(fileData: string) { + let contentTag: typeof import('content-tag'); + try { + contentTag = require('content-tag'); + } catch { + throw MissingGlimmerCompilerError; + } + + return transformGlimmerFile(contentTag, fileData); +} diff --git a/packages/graphql-tag-pluck/tests/graphql-tag-pluck.test.ts b/packages/graphql-tag-pluck/tests/graphql-tag-pluck.test.ts index e67f78d8c5a..1b711e3fcc9 100644 --- a/packages/graphql-tag-pluck/tests/graphql-tag-pluck.test.ts +++ b/packages/graphql-tag-pluck/tests/graphql-tag-pluck.test.ts @@ -2393,5 +2393,277 @@ describe('graphql-tag-pluck', () => { 'query queryName { id }\n#EXPRESSION:ANOTHER_VARIABLE', ); }); + + it('should pluck graphql-tag template literals from .gts file', async () => { + const sources = await pluck( + 'tmp-XXXXXX.gts', + freeText(` + import Component from '@glimmer/component'; + import graphql from 'graphql-tag'; + + const UpdateCreditCardMutationDocument = graphql(\` + mutation updateCreditCard($input: UpdateCreditCardInput!) { + updateCreditCard(input: $input) { + __typename + } + } + \`); + + + export default class PaymentDetailsPage extends Component { + updateCreditCardMutation = async (): Promise => { + return await new Promise((resolve) => resolve()); + } + + onSubmit = async (): Promise => { + return this.updateCreditCardMutation(); + } + + + } + `), + ); + + expect(sources.map(source => source.body).join('\n\n')).toEqual( + freeText(` + mutation updateCreditCard($input: UpdateCreditCardInput!) { + updateCreditCard(input: $input) { + __typename + } + } + `), + ); + }); + + it('should pluck graphql-tag template literals from .gjs file', async () => { + const sources = await pluck( + 'tmp-XXXXXX.gjs', + freeText(` + import Component from '@glimmer/component'; + import graphql from 'graphql-tag'; + + const UpdateCreditCardMutationDocument = graphql(\` + mutation updateCreditCard($input: UpdateCreditCardInput!) { + updateCreditCard(input: $input) { + __typename + } + } + \`); + + + export default class PaymentDetailsPage extends Component { + updateCreditCardMutation = async (): Promise => { + return await new Promise((resolve) => resolve()); + } + + onSubmit = async (): Promise => { + return this.updateCreditCardMutation(); + } + + + } + `), + ); + + expect(sources.map(source => source.body).join('\n\n')).toEqual( + freeText(` + mutation updateCreditCard($input: UpdateCreditCardInput!) { + updateCreditCard(input: $input) { + __typename + } + } + `), + ); + }); + + it('should pluck graphql-tag template literals from .gts file with 2 queries', async () => { + const sources = await pluck( + 'tmp-XXXXXX.gts', + freeText(` + import Component from '@glimmer/component'; + import graphql from 'graphql-tag' + + const UpdateCreditCardMutationDocument = graphql(\` + mutation updateCreditCard($input: UpdateCreditCardInput!) { + updateCreditCard(input: $input) { + __typename + } + } + \`); + + const UpdatePaypalMutationDocument = graphql(\` + mutation updatePaypal($input: UpdatePaypalInput!) { + updatePaypal(input: $input) { + __typename + } + } + \`); + + + export default class PaymentDetailsPage extends Component { + updateCreditCardMutation = async (): Promise => { + return await new Promise((resolve) => resolve()); + } + + onSubmit = async (): Promise => { + return this.updateCreditCardMutation(); + } + + + } + `), + ); + + expect(sources.map(source => source.body).join('\n\n')).toEqual( + freeText(` + mutation updateCreditCard($input: UpdateCreditCardInput!) { + updateCreditCard(input: $input) { + __typename + } + } + + mutation updatePaypal($input: UpdatePaypalInput!) { + updatePaypal(input: $input) { + __typename + } + } + `), + ); + }); + + it('should pluck graphql-tag template literals from .gts file with multiple queries in different function signatures', async () => { + const sources = await pluck( + 'tmp-XXXXXX.gts', + freeText(` + import Component from '@glimmer/component'; + import graphql from 'graphql-tag' + + const UpdateCreditCardMutationDocument = graphql(\` + mutation updateCreditCard($input: UpdateCreditCardInput!) { + updateCreditCard(input: $input) { + __typename + } + } + \`); + + export function anotherQuery() { + const UpdatePaypalMutationDocument = graphql(\` + mutation updatePaypal($input: UpdatePaypalInput!) { + updatePaypal(input: $input) { + __typename + } + } + \`); + } + + export default class PaymentDetailsPage extends Component { + updateCreditCardMutation = async (): Promise => { + return await new Promise((resolve) => resolve()); + } + + onSubmit = async (): Promise => { + return this.updateCreditCardMutation(); + } + + + } + `), + ); + + expect(sources.map(source => source.body).join('\n\n')).toEqual( + freeText(` + mutation updateCreditCard($input: UpdateCreditCardInput!) { + updateCreditCard(input: $input) { + __typename + } + } + + mutation updatePaypal($input: UpdatePaypalInput!) { + updatePaypal(input: $input) { + __typename + } + } + `), + ); + }); + + it('should pluck graphql-tag template literals from .gts file, ignoring comments', async () => { + const sources = await pluck( + 'tmp-XXXXXX.gts', + freeText(` + import Component from '@glimmer/component'; + import graphql from 'graphql-tag' + + const UpdateCreditCardMutationDocument = graphql(\` + mutation updateCreditCard($input: UpdateCreditCardInput!) { + updateCreditCard(input: $input) { + __typename + } + } + \`); + + // const UpdatePaypalMutationDocument = graphql(\` + // mutation updatePaypal($input: UpdatePaypalInput!) { + // updatePaypal(input: $input) { + // __typename + // } + // } + // \`); + + + export default class PaymentDetailsPage extends Component { + updateCreditCardMutation = async (): Promise => { + return await new Promise((resolve) => resolve()); + } + + onSubmit = async (): Promise => { + return this.updateCreditCardMutation(); + } + + + } + `), + ); + + expect(sources.map(source => source.body).join('\n\n')).toEqual( + freeText(` + mutation updateCreditCard($input: UpdateCreditCardInput!) { + updateCreditCard(input: $input) { + __typename + } + } + `), + ); + }); }); }); diff --git a/packages/loaders/code-file/src/index.ts b/packages/loaders/code-file/src/index.ts index b5cc486a3ed..0fd8f2ac610 100644 --- a/packages/loaders/code-file/src/index.ts +++ b/packages/loaders/code-file/src/index.ts @@ -55,6 +55,8 @@ const FILE_EXTENSIONS = [ '.vue', '.svelte', '.astro', + '.gts', + '.gjs', ]; function createGlobbyOptions(options: CodeFileLoaderOptions): GlobbyOptions { @@ -76,7 +78,7 @@ const buildIgnoreGlob = (path: string) => `!${path}`; * ``` * * Supported extensions include: `.ts`, `.mts`, `.cts`, `.tsx`, `.js`, `.mjs`, - * `.cjs`, `.jsx`, `.vue`, `.svelte`, `.astro` + * `.cjs`, `.jsx`, `.vue`, `.svelte`, `.astro`, `.gts`, `.gjs`. */ export class CodeFileLoader implements Loader { private config: CodeFileLoaderConfig; diff --git a/yarn.lock b/yarn.lock index 5c98ab49623..b45b5d7ebcb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4482,6 +4482,11 @@ content-disposition@0.5.4: dependencies: safe-buffer "5.2.1" +content-tag@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/content-tag/-/content-tag-2.0.1.tgz#0b908ed97e13df60b019039713ab63f1b73f2b22" + integrity sha512-jxsETSDs5NbNwyiDuIp672fUMhUyu8Qxc5MOBOJOcgW/fQESI6o5K1LBDrnEE7Bh810a685lWEZHTF4jQYGEEw== + content-type@~1.0.4, content-type@~1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" @@ -11539,16 +11544,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -11611,14 +11607,7 @@ stringify-entities@^4.0.0: character-entities-html4 "^2.0.0" character-entities-legacy "^3.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -12763,7 +12752,7 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -12781,15 +12770,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"