Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 61 additions & 11 deletions packages/graphql-tag-pluck/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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);
}

Expand Down
69 changes: 69 additions & 0 deletions packages/graphql-tag-pluck/tests/graphql-tag-pluck.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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(`
<template>
<div>test</div>
</template>

<script setup lang="ts">
import gql from 'graphql-tag';

const pageQuery = gql\`
query IndexQuery {
site {
siteMetadata {
title
}
}
}
\`;

import { ExternalProps } from './ExternalProps';
const props = defineProps<ExternalProps>();
</script>
`);

// 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',
Expand Down
Loading