diff --git a/packages/graphql-tag-pluck/src/index.ts b/packages/graphql-tag-pluck/src/index.ts index 5affe6b95b3..1cdfb04f011 100644 --- a/packages/graphql-tag-pluck/src/index.ts +++ b/packages/graphql-tag-pluck/src/index.ts @@ -153,8 +153,21 @@ const supportedExtensions = [ ]; // tslint:disable-next-line: no-implicit-dependencies -function parseWithVue(vueTemplateCompiler: typeof import('@vue/compiler-sfc'), fileData: string) { - const { descriptor } = vueTemplateCompiler.parse(fileData); +function parseWithVue( + vueTemplateCompiler: typeof import('@vue/compiler-sfc'), + typescriptPackage: typeof import('typescript'), + fileData: string, + filePath: string, +) { + // Calls to registerTS are idempotent, so it's safe to call it repeatedly like + // we are here. + // + // See https://github.com/ardatan/graphql-tools/pull/7271 for more details. + // + + vueTemplateCompiler.registerTS(() => typescriptPackage); + + const { descriptor } = vueTemplateCompiler.parse(fileData, { filename: filePath }); return descriptor.script || descriptor.scriptSetup ? vueTemplateCompiler.compileScript(descriptor, { id: Date.now().toString() }).content @@ -168,7 +181,7 @@ function customBlockFromVue( filePath: string, blockType: string, ): Source | undefined { - const { descriptor } = vueTemplateCompiler.parse(fileData); + const { descriptor } = vueTemplateCompiler.parse(fileData, { filename: filePath }); const block = descriptor.customBlocks.find(b => b.type === blockType); if (block === undefined) { @@ -232,7 +245,7 @@ export const gqlPluckFromCodeString = async ( if (options.gqlVueBlock) { blockSource = await pluckVueFileCustomBlock(code, filePath, options.gqlVueBlock); } - code = await pluckVueFileScript(code); + code = await pluckVueFileScript(code, filePath); } else if (fileExt === '.svelte') { code = await pluckSvelteFileScript(code); } else if (fileExt === '.astro') { @@ -273,7 +286,7 @@ export const gqlPluckFromCodeStringSync = ( if (options.gqlVueBlock) { blockSource = pluckVueFileCustomBlockSync(code, filePath, options.gqlVueBlock); } - code = pluckVueFileScriptSync(code); + code = pluckVueFileScriptSync(code, filePath); } else if (fileExt === '.svelte') { code = pluckSvelteFileScriptSync(code); } else if (fileExt === '.astro') { @@ -391,6 +404,21 @@ const MissingGlimmerCompilerError = new Error( `), ); +const MissingTypeScriptPackageError = new Error( + freeText(` + GraphQL template literals cannot be plucked from a Vue template code without having the "typescript" package installed. + Please install it and try again. + + Via NPM: + + $ npm install typescript + + Via Yarn: + + $ yarn add typescript + `), +); + async function loadVueCompilerAsync() { try { // eslint-disable-next-line import/no-extraneous-dependencies @@ -400,6 +428,15 @@ async function loadVueCompilerAsync() { } } +async function loadTypeScriptPackageAsync() { + try { + // eslint-disable-next-line import/no-extraneous-dependencies + return await import('typescript'); + } catch { + throw MissingTypeScriptPackageError; + } +} + function loadVueCompilerSync() { try { // eslint-disable-next-line import/no-extraneous-dependencies @@ -409,18 +446,31 @@ function loadVueCompilerSync() { } } -async function pluckVueFileScript(fileData: string) { - const vueTemplateCompiler = await loadVueCompilerAsync(); - return parseWithVue(vueTemplateCompiler, fileData); +function loadTypeScriptPackageSync() { + try { + // eslint-disable-next-line import/no-extraneous-dependencies + return require('typescript'); + } catch { + throw MissingTypeScriptPackageError; + } } -function pluckVueFileScriptSync(fileData: string) { +async function pluckVueFileScript(fileData: string, filePath: string) { + const [typescriptPackage, vueTemplateCompiler] = await Promise.all([ + loadTypeScriptPackageAsync(), + loadVueCompilerAsync(), + ]); + return parseWithVue(vueTemplateCompiler, typescriptPackage, fileData, filePath); +} + +function pluckVueFileScriptSync(fileData: string, filePath: string) { const vueTemplateCompiler = loadVueCompilerSync(); - return parseWithVue(vueTemplateCompiler, fileData); + const typescriptPackage = loadTypeScriptPackageSync(); + return parseWithVue(vueTemplateCompiler, typescriptPackage, fileData, filePath); } async function pluckVueFileCustomBlock(fileData: string, filePath: string, blockType: string) { - const vueTemplateCompiler = await loadVueCompilerSync(); + const vueTemplateCompiler = await loadVueCompilerAsync(); return customBlockFromVue(vueTemplateCompiler, fileData, filePath, blockType); } 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 568c4bfe7f7..0d9a548d90b 100644 --- a/packages/graphql-tag-pluck/tests/graphql-tag-pluck.test.ts +++ b/packages/graphql-tag-pluck/tests/graphql-tag-pluck.test.ts @@ -1,7 +1,23 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; import { runTests } from '../../testing/utils.js'; import { gqlPluckFromCodeString, gqlPluckFromCodeStringSync } from '../src/index.js'; import { freeText } from '../src/utils.js'; +// A temporary directory unique for each unit test. Cleaned up after each unit +// test resolves. +let tmpDir: string; + +beforeEach(async () => { + // We create temporary directories in the test directory because our test + // infrastructure denies writes to the host's tmp directory. + tmpDir = await fs.mkdtemp(path.join(__dirname, 'tmp-')); +}); + +afterEach(async () => { + await fs.rm(tmpDir, { recursive: true }); +}); + describe('graphql-tag-pluck', () => { runTests({ async: gqlPluckFromCodeString, @@ -852,6 +868,59 @@ describe('graphql-tag-pluck', () => { ); }); + it('should pluck graphql-tag template literals from .vue 3 setup with compiler macros and imports', async () => { + const EXTERNAL_PROPS_SOURCE = freeText(` + export type ExternalProps = { + foo: string; + }; + `); + + const VUE_SFC_SOURCE = freeText(` + + + + `); + + // We must write the files to disk because this test is specifically + // ensuring that imports work in Vue SFC files with compiler macros and + // imports are resolved on disk by the typescript runtime. + // + // See https://github.com/ardatan/graphql-tools/pull/7271 for details. + await fs.writeFile(path.join(tmpDir, 'ExternalProps.ts'), EXTERNAL_PROPS_SOURCE); + await fs.writeFile(path.join(tmpDir, 'component.vue'), VUE_SFC_SOURCE); + + const sources = await pluck(path.join(tmpDir, 'component.vue'), VUE_SFC_SOURCE); + + expect(sources.map(source => source.body).join('\n\n')).toEqual( + freeText(` + query IndexQuery { + site { + siteMetadata { + title + } + } + } + `), + ); + }); + it('should pluck graphql-tag template literals from .vue 3 setup JavaScript file', async () => { const sources = await pluck( 'tmp-XXXXXX.vue',