Skip to content

Commit e77f0cf

Browse files
HunterLarcoardatan
andcommitted
support parsing vue single-file-components using composition API (#7271)
* fix vue files * register-ts * test * self review * resolve github runner permissions issue * satisfy lint * revert package.json as per comments * adopt peerDependenciesMeta * revert package.json * resolve lint * Better module loading * Align --------- Co-authored-by: Arda TANRIKULU <ardatanrikulu@gmail.com>
1 parent 541f034 commit e77f0cf

File tree

2 files changed

+130
-11
lines changed

2 files changed

+130
-11
lines changed

packages/graphql-tag-pluck/src/index.ts

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,21 @@ const supportedExtensions = [
153153
];
154154

155155
// tslint:disable-next-line: no-implicit-dependencies
156-
function parseWithVue(vueTemplateCompiler: typeof import('@vue/compiler-sfc'), fileData: string) {
157-
const { descriptor } = vueTemplateCompiler.parse(fileData);
156+
function parseWithVue(
157+
vueTemplateCompiler: typeof import('@vue/compiler-sfc'),
158+
typescriptPackage: typeof import('typescript'),
159+
fileData: string,
160+
filePath: string,
161+
) {
162+
// Calls to registerTS are idempotent, so it's safe to call it repeatedly like
163+
// we are here.
164+
//
165+
// See https://github.com/ardatan/graphql-tools/pull/7271 for more details.
166+
//
167+
168+
vueTemplateCompiler.registerTS(() => typescriptPackage);
169+
170+
const { descriptor } = vueTemplateCompiler.parse(fileData, { filename: filePath });
158171

159172
return descriptor.script || descriptor.scriptSetup
160173
? vueTemplateCompiler.compileScript(descriptor, { id: Date.now().toString() }).content
@@ -168,7 +181,7 @@ function customBlockFromVue(
168181
filePath: string,
169182
blockType: string,
170183
): Source | undefined {
171-
const { descriptor } = vueTemplateCompiler.parse(fileData);
184+
const { descriptor } = vueTemplateCompiler.parse(fileData, { filename: filePath });
172185

173186
const block = descriptor.customBlocks.find(b => b.type === blockType);
174187
if (block === undefined) {
@@ -232,7 +245,7 @@ export const gqlPluckFromCodeString = async (
232245
if (options.gqlVueBlock) {
233246
blockSource = await pluckVueFileCustomBlock(code, filePath, options.gqlVueBlock);
234247
}
235-
code = await pluckVueFileScript(code);
248+
code = await pluckVueFileScript(code, filePath);
236249
} else if (fileExt === '.svelte') {
237250
code = await pluckSvelteFileScript(code);
238251
} else if (fileExt === '.astro') {
@@ -273,7 +286,7 @@ export const gqlPluckFromCodeStringSync = (
273286
if (options.gqlVueBlock) {
274287
blockSource = pluckVueFileCustomBlockSync(code, filePath, options.gqlVueBlock);
275288
}
276-
code = pluckVueFileScriptSync(code);
289+
code = pluckVueFileScriptSync(code, filePath);
277290
} else if (fileExt === '.svelte') {
278291
code = pluckSvelteFileScriptSync(code);
279292
} else if (fileExt === '.astro') {
@@ -391,6 +404,21 @@ const MissingGlimmerCompilerError = new Error(
391404
`),
392405
);
393406

407+
const MissingTypeScriptPackageError = new Error(
408+
freeText(`
409+
GraphQL template literals cannot be plucked from a Vue template code without having the "typescript" package installed.
410+
Please install it and try again.
411+
412+
Via NPM:
413+
414+
$ npm install typescript
415+
416+
Via Yarn:
417+
418+
$ yarn add typescript
419+
`),
420+
);
421+
394422
async function loadVueCompilerAsync() {
395423
try {
396424
// eslint-disable-next-line import/no-extraneous-dependencies
@@ -400,6 +428,15 @@ async function loadVueCompilerAsync() {
400428
}
401429
}
402430

431+
async function loadTypeScriptPackageAsync() {
432+
try {
433+
// eslint-disable-next-line import/no-extraneous-dependencies
434+
return await import('typescript');
435+
} catch {
436+
throw MissingTypeScriptPackageError;
437+
}
438+
}
439+
403440
function loadVueCompilerSync() {
404441
try {
405442
// eslint-disable-next-line import/no-extraneous-dependencies
@@ -409,18 +446,31 @@ function loadVueCompilerSync() {
409446
}
410447
}
411448

412-
async function pluckVueFileScript(fileData: string) {
413-
const vueTemplateCompiler = await loadVueCompilerAsync();
414-
return parseWithVue(vueTemplateCompiler, fileData);
449+
function loadTypeScriptPackageSync() {
450+
try {
451+
// eslint-disable-next-line import/no-extraneous-dependencies
452+
return require('typescript');
453+
} catch {
454+
throw MissingTypeScriptPackageError;
455+
}
415456
}
416457

417-
function pluckVueFileScriptSync(fileData: string) {
458+
async function pluckVueFileScript(fileData: string, filePath: string) {
459+
const [typescriptPackage, vueTemplateCompiler] = await Promise.all([
460+
loadTypeScriptPackageAsync(),
461+
loadVueCompilerAsync(),
462+
]);
463+
return parseWithVue(vueTemplateCompiler, typescriptPackage, fileData, filePath);
464+
}
465+
466+
function pluckVueFileScriptSync(fileData: string, filePath: string) {
418467
const vueTemplateCompiler = loadVueCompilerSync();
419-
return parseWithVue(vueTemplateCompiler, fileData);
468+
const typescriptPackage = loadTypeScriptPackageSync();
469+
return parseWithVue(vueTemplateCompiler, typescriptPackage, fileData, filePath);
420470
}
421471

422472
async function pluckVueFileCustomBlock(fileData: string, filePath: string, blockType: string) {
423-
const vueTemplateCompiler = await loadVueCompilerSync();
473+
const vueTemplateCompiler = await loadVueCompilerAsync();
424474
return customBlockFromVue(vueTemplateCompiler, fileData, filePath, blockType);
425475
}
426476

packages/graphql-tag-pluck/tests/graphql-tag-pluck.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
1+
import fs from 'node:fs/promises';
2+
import path from 'node:path';
13
import { runTests } from '../../testing/utils.js';
24
import { gqlPluckFromCodeString, gqlPluckFromCodeStringSync } from '../src/index.js';
35
import { freeText } from '../src/utils.js';
46

7+
// A temporary directory unique for each unit test. Cleaned up after each unit
8+
// test resolves.
9+
let tmpDir: string;
10+
11+
beforeEach(async () => {
12+
// We create temporary directories in the test directory because our test
13+
// infrastructure denies writes to the host's tmp directory.
14+
tmpDir = await fs.mkdtemp(path.join(__dirname, 'tmp-'));
15+
});
16+
17+
afterEach(async () => {
18+
await fs.rm(tmpDir, { recursive: true });
19+
});
20+
521
describe('graphql-tag-pluck', () => {
622
runTests({
723
async: gqlPluckFromCodeString,
@@ -852,6 +868,59 @@ describe('graphql-tag-pluck', () => {
852868
);
853869
});
854870

871+
it('should pluck graphql-tag template literals from .vue 3 setup with compiler macros and imports', async () => {
872+
const EXTERNAL_PROPS_SOURCE = freeText(`
873+
export type ExternalProps = {
874+
foo: string;
875+
};
876+
`);
877+
878+
const VUE_SFC_SOURCE = freeText(`
879+
<template>
880+
<div>test</div>
881+
</template>
882+
883+
<script setup lang="ts">
884+
import gql from 'graphql-tag';
885+
886+
const pageQuery = gql\`
887+
query IndexQuery {
888+
site {
889+
siteMetadata {
890+
title
891+
}
892+
}
893+
}
894+
\`;
895+
896+
import { ExternalProps } from './ExternalProps';
897+
const props = defineProps<ExternalProps>();
898+
</script>
899+
`);
900+
901+
// We must write the files to disk because this test is specifically
902+
// ensuring that imports work in Vue SFC files with compiler macros and
903+
// imports are resolved on disk by the typescript runtime.
904+
//
905+
// See https://github.com/ardatan/graphql-tools/pull/7271 for details.
906+
await fs.writeFile(path.join(tmpDir, 'ExternalProps.ts'), EXTERNAL_PROPS_SOURCE);
907+
await fs.writeFile(path.join(tmpDir, 'component.vue'), VUE_SFC_SOURCE);
908+
909+
const sources = await pluck(path.join(tmpDir, 'component.vue'), VUE_SFC_SOURCE);
910+
911+
expect(sources.map(source => source.body).join('\n\n')).toEqual(
912+
freeText(`
913+
query IndexQuery {
914+
site {
915+
siteMetadata {
916+
title
917+
}
918+
}
919+
}
920+
`),
921+
);
922+
});
923+
855924
it('should pluck graphql-tag template literals from .vue 3 setup JavaScript file', async () => {
856925
const sources = await pluck(
857926
'tmp-XXXXXX.vue',

0 commit comments

Comments
 (0)