Skip to content

Commit

Permalink
happy svelte parsing (#3366)
Browse files Browse the repository at this point in the history
* happy svelte parsing
* test range
* refactor tag parsing for readability & perf tweaks
  • Loading branch information
acao committed Jan 7, 2024
1 parent 9b6ce25 commit 22771f3
Show file tree
Hide file tree
Showing 18 changed files with 502 additions and 266 deletions.
7 changes: 7 additions & 0 deletions .changeset/slimy-fireants-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'graphql-language-service-cli': patch
'graphql-language-service-server': patch
'vscode-graphql': patch
---

Fixes to svelte parsing, tag parsing refactor
2 changes: 0 additions & 2 deletions .github/workflows/main-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,3 @@ jobs:
files: coverage/lcov.info
fail_ci_if_error: true
verbose: true


2 changes: 2 additions & 0 deletions jest.config.base.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ module.exports = (dir, env = 'jsdom') => {
'^cm6-graphql\\/src\\/([^]+)': `${__dirname}/packages/cm6-graphql/dist/$1`,
'^example-([^/]+)': `${__dirname}/examples/$1/src`,
'^-!svg-react-loader.*$': '<rootDir>/resources/jest/svgImportMock.js',
// because of the svelte compiler's export patterns i guess?
'svelte/compiler': `${__dirname}/node_modules/svelte/compiler.cjs`,
},
testMatch: ['**/*[-.](spec|test).[jt]s?(x)', '!**/cypress/**'],
testEnvironment: env,
Expand Down
9 changes: 5 additions & 4 deletions packages/graphql-language-service-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,13 @@
"mkdirp": "^1.0.4",
"node-abort-controller": "^3.0.1",
"nullthrows": "^1.0.0",
"svelte": "^4.0.0",
"svelte2tsx": "^0.6.16",
"vscode-jsonrpc": "^8.0.1",
"vscode-languageserver": "^8.0.1",
"vscode-languageserver-types": "^3.17.1",
"vscode-uri": "^3.0.2"
"vscode-languageserver-types": "^3.17.2",
"vscode-uri": "^3.0.2",
"svelte2tsx": "^0.6.19",
"svelte": "^4.1.1",
"source-map-js": "1.0.2"
},
"devDependencies": {
"@types/glob": "^8.1.0",
Expand Down
20 changes: 15 additions & 5 deletions packages/graphql-language-service-server/src/GraphQLCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import type {
ObjectTypeInfo,
Uri,
} from 'graphql-language-service';
import type { Logger } from 'vscode-languageserver';

import * as fs from 'node:fs';
import { readFile } from 'node:fs/promises';
Expand All @@ -48,6 +47,11 @@ import glob from 'glob';
import { LoadConfigOptions } from './types';
import { URI } from 'vscode-uri';
import { CodeFileLoader } from '@graphql-tools/code-file-loader';
import {
DEFAULT_SUPPORTED_EXTENSIONS,
DEFAULT_SUPPORTED_GRAPHQL_EXTENSIONS,
} from './constants';
import { NoopLogger, Logger } from './Logger';

const LanguageServiceExtension: GraphQLExtensionDeclaration = api => {
// For schema
Expand All @@ -68,7 +72,7 @@ export async function getGraphQLCache({
config,
}: {
parser: typeof parseDocument;
logger: Logger;
logger: Logger | NoopLogger;
loadConfigOptions: LoadConfigOptions;
config?: GraphQLConfig;
}): Promise<GraphQLCache> {
Expand Down Expand Up @@ -98,7 +102,7 @@ export class GraphQLCache implements GraphQLCacheInterface {
_fragmentDefinitionsCache: Map<Uri, Map<string, FragmentInfo>>;
_typeDefinitionsCache: Map<Uri, Map<string, ObjectTypeInfo>>;
_parser: typeof parseDocument;
_logger: Logger;
_logger: Logger | NoopLogger;

constructor({
configDir,
Expand All @@ -109,7 +113,7 @@ export class GraphQLCache implements GraphQLCacheInterface {
configDir: Uri;
config: GraphQLConfig;
parser: typeof parseDocument;
logger: Logger;
logger: Logger | NoopLogger;
}) {
this._configDir = configDir;
this._graphQLConfig = config;
Expand Down Expand Up @@ -827,7 +831,13 @@ export class GraphQLCache implements GraphQLCacheInterface {
let queries: CachedContent[] = [];
if (content.trim().length !== 0) {
try {
queries = this._parser(content, filePath);
queries = this._parser(
content,
filePath,
DEFAULT_SUPPORTED_EXTENSIONS,
DEFAULT_SUPPORTED_GRAPHQL_EXTENSIONS,
this._logger,
);
if (queries.length === 0) {
// still resolve with an empty ast
return {
Expand Down
16 changes: 10 additions & 6 deletions packages/graphql-language-service-server/src/MessageProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,12 @@ import type {
WorkspaceSymbolParams,
Connection,
DidChangeConfigurationRegistrationOptions,
Logger,
} from 'vscode-languageserver/node';

import type { UnnormalizedTypeDefPointer } from '@graphql-tools/load';

import { getGraphQLCache, GraphQLCache } from './GraphQLCache';
import { parseDocument, DEFAULT_SUPPORTED_EXTENSIONS } from './parseDocument';
import { parseDocument } from './parseDocument';

import { printSchema, visit, parse, FragmentDefinitionNode } from 'graphql';
import { tmpdir } from 'node:os';
Expand All @@ -72,6 +71,11 @@ import {
ProjectNotFoundError,
} from 'graphql-config';
import type { LoadConfigOptions } from './types';
import {
DEFAULT_SUPPORTED_EXTENSIONS,
SupportedExtensionsEnum,
} from './constants';
import { NoopLogger, Logger } from './Logger';

const configDocLink =
'https://www.npmjs.com/package/graphql-language-service-server#user-content-graphql-configuration-file';
Expand All @@ -93,7 +97,7 @@ export class MessageProcessor {
_isInitialized = false;
_isGraphQLConfigMissing: boolean | null = null;
_willShutdown = false;
_logger: Logger;
_logger: Logger | NoopLogger;
_extensions?: GraphQLExtensionDeclaration[];
_parser: (text: string, uri: string) => CachedContent[];
_tmpDir: string;
Expand All @@ -114,8 +118,8 @@ export class MessageProcessor {
tmpDir,
connection,
}: {
logger: Logger;
fileExtensions: string[];
logger: Logger | NoopLogger;
fileExtensions: ReadonlyArray<SupportedExtensionsEnum>;
graphqlFileExtensions: string[];
loadConfigOptions: LoadConfigOptions;
config?: GraphQLConfig;
Expand Down Expand Up @@ -788,7 +792,7 @@ export class MessageProcessor {
if (parentRange && res.name) {
const isInline = inlineFragments.includes(res.name);
const isEmbedded = DEFAULT_SUPPORTED_EXTENSIONS.includes(
path.extname(textDocument.uri),
path.extname(textDocument.uri) as SupportedExtensionsEnum,
);
if (isInline && isEmbedded) {
const vOffset = parentRange.start.line;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ describe('MessageProcessor', () => {
// @ts-ignore
connection: {},
logger,
fileExtensions: ['js'],
graphqlFileExtensions: ['graphql'],
loadConfigOptions: { rootDir: __dirname },
});
Expand All @@ -56,6 +55,7 @@ describe('MessageProcessor', () => {
configDir: __dirname,
config: gqlConfig,
parser: parseDocument,
logger: new NoopLogger(),
});
messageProcessor._languageService = {
// @ts-ignore
Expand Down Expand Up @@ -484,6 +484,7 @@ export function Example(arg: string) {
}`;

const contents = parseDocument(text, 'test.tsx');

expect(contents[0].query).toEqual(`
query Test {
test {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@
*
*/

import { Position, Range } from 'graphql-language-service';
import { findGraphQLTags as baseFindGraphQLTags } from '../findGraphQLTags';

jest.mock('../Logger');

import { NoopLogger } from '../Logger';
import { SupportedExtensionsEnum } from '../constants';

describe('findGraphQLTags', () => {
const logger = new NoopLogger();
const findGraphQLTags = (text: string, ext: string) =>
const findGraphQLTags = (text: string, ext: SupportedExtensionsEnum) =>
baseFindGraphQLTags(text, ext, '', logger);

it('finds queries in tagged templates', async () => {
Expand Down Expand Up @@ -315,15 +317,46 @@ query {id}`);

it('finds queries in tagged templates in Svelte using normal <script>', async () => {
const text = `
<script>
gql\`
query {id}
\`;
<script context="module">
const query = graphql(\`
query AllCharacters {
characters {
results {
name
id
image
}
}
}
\`)
export async function load({fetch}) {
return {
props: {
_data: await fetch({
text: query
})
}
}
}
</script>
`;
const contents = findGraphQLTags(text, '.svelte');
expect(contents[0].template).toEqual(`
query {id}`);
query AllCharacters {
characters {
results {
name
id
image
}
}
}
`);

expect(JSON.stringify(contents[0].range)).toEqual(
JSON.stringify(new Range(new Position(2, 29), new Position(12, 0))),
);
});

it('no crash in Svelte files without <script>', async () => {
Expand Down
100 changes: 100 additions & 0 deletions packages/graphql-language-service-server/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import type { ParserOptions, ParserPlugin } from '@babel/parser';
// Attempt to be as inclusive as possible of source text.
export const PARSER_OPTIONS: ParserOptions = {
allowImportExportEverywhere: true,
allowReturnOutsideFunction: true,
allowSuperOutsideMethod: true,
allowAwaitOutsideFunction: true,
// important! this allows babel to keep parsing when there are issues
errorRecovery: true,
sourceType: 'module',
strictMode: false,
};

/**
* .graphql is the officially recommended extension for graphql files
*
* .gql and .graphqls are included for compatibility for commonly used extensions
*
* GQL is a registered trademark of Google, and refers to Google Query Language.
* GraphQL Foundation does *not* recommend using this extension or acronym for
* referring to GraphQL.
*
* any changes should also be reflected in vscode-graphql-syntax textmate grammar & package.json
*/
export const DEFAULT_SUPPORTED_GRAPHQL_EXTENSIONS = [
'.graphql',
'.graphqls',
'.gql',
];

/**
* default tag delimiters to use when parsing GraphQL strings (for js/ts/vue/svelte)
* any changes should also be reflected in vscode-graphql-syntax textmate grammar
*/
export const TAG_MAP: Record<string, true> = {
graphql: true,
gql: true,
graphqls: true,
};

/**
* default extensions to use when parsing for GraphQL strings
* any changes should also be reflected in vscode-graphql-syntax textmate grammar & package.json
*/
export const DEFAULT_SUPPORTED_EXTENSIONS = [
'.js',
'.cjs',
'.mjs',
'.es',
'.esm',
'.es6',
'.ts',
'.jsx',
'.tsx',
'.vue',
'.svelte',
'.cts',
'.mts',
] as const;
export type SupportedExtensions = typeof DEFAULT_SUPPORTED_EXTENSIONS;
export type SupportedExtensionsEnum =
(typeof DEFAULT_SUPPORTED_EXTENSIONS)[number];

/**
* default plugins to use with babel parser
*/
export const BABEL_PLUGINS: ParserPlugin[] = [
'asyncDoExpressions',
'asyncGenerators',
'bigInt',
'classProperties',
'classPrivateProperties',
'classPrivateMethods',
'classStaticBlock',
'doExpressions',
'decimal',
'decorators-legacy',
'destructuringPrivate',
'dynamicImport',
'exportDefaultFrom',
'exportNamespaceFrom',
'functionBind',
'functionSent',
'importMeta',
'importAssertions',
'jsx',
'logicalAssignment',
'moduleBlocks',
'moduleStringNames',
'nullishCoalescingOperator',
'numericSeparator',
'objectRestSpread',
'optionalCatchBinding',
'optionalChaining',
// ['pipelineOperator', { proposal: 'hack' }],
'privateIn',
'regexpUnicodeSets',
'throwExpressions',
'topLevelAwait',
];
Loading

0 comments on commit 22771f3

Please sign in to comment.