Skip to content

Commit 22771f3

Browse files
committed
happy svelte parsing (#3366)
* happy svelte parsing * test range * refactor tag parsing for readability & perf tweaks
1 parent 9b6ce25 commit 22771f3

File tree

18 files changed

+502
-266
lines changed

18 files changed

+502
-266
lines changed

.changeset/slimy-fireants-dream.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'graphql-language-service-cli': patch
3+
'graphql-language-service-server': patch
4+
'vscode-graphql': patch
5+
---
6+
7+
Fixes to svelte parsing, tag parsing refactor

.github/workflows/main-test.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,3 @@ jobs:
5151
files: coverage/lcov.info
5252
fail_ci_if_error: true
5353
verbose: true
54-
55-

jest.config.base.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ module.exports = (dir, env = 'jsdom') => {
3030
'^cm6-graphql\\/src\\/([^]+)': `${__dirname}/packages/cm6-graphql/dist/$1`,
3131
'^example-([^/]+)': `${__dirname}/examples/$1/src`,
3232
'^-!svg-react-loader.*$': '<rootDir>/resources/jest/svgImportMock.js',
33+
// because of the svelte compiler's export patterns i guess?
34+
'svelte/compiler': `${__dirname}/node_modules/svelte/compiler.cjs`,
3335
},
3436
testMatch: ['**/*[-.](spec|test).[jt]s?(x)', '!**/cypress/**'],
3537
testEnvironment: env,

packages/graphql-language-service-server/package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,13 @@
5050
"mkdirp": "^1.0.4",
5151
"node-abort-controller": "^3.0.1",
5252
"nullthrows": "^1.0.0",
53-
"svelte": "^4.0.0",
54-
"svelte2tsx": "^0.6.16",
5553
"vscode-jsonrpc": "^8.0.1",
5654
"vscode-languageserver": "^8.0.1",
57-
"vscode-languageserver-types": "^3.17.1",
58-
"vscode-uri": "^3.0.2"
55+
"vscode-languageserver-types": "^3.17.2",
56+
"vscode-uri": "^3.0.2",
57+
"svelte2tsx": "^0.6.19",
58+
"svelte": "^4.1.1",
59+
"source-map-js": "1.0.2"
5960
},
6061
"devDependencies": {
6162
"@types/glob": "^8.1.0",

packages/graphql-language-service-server/src/GraphQLCache.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import type {
2727
ObjectTypeInfo,
2828
Uri,
2929
} from 'graphql-language-service';
30-
import type { Logger } from 'vscode-languageserver';
3130

3231
import * as fs from 'node:fs';
3332
import { readFile } from 'node:fs/promises';
@@ -48,6 +47,11 @@ import glob from 'glob';
4847
import { LoadConfigOptions } from './types';
4948
import { URI } from 'vscode-uri';
5049
import { CodeFileLoader } from '@graphql-tools/code-file-loader';
50+
import {
51+
DEFAULT_SUPPORTED_EXTENSIONS,
52+
DEFAULT_SUPPORTED_GRAPHQL_EXTENSIONS,
53+
} from './constants';
54+
import { NoopLogger, Logger } from './Logger';
5155

5256
const LanguageServiceExtension: GraphQLExtensionDeclaration = api => {
5357
// For schema
@@ -68,7 +72,7 @@ export async function getGraphQLCache({
6872
config,
6973
}: {
7074
parser: typeof parseDocument;
71-
logger: Logger;
75+
logger: Logger | NoopLogger;
7276
loadConfigOptions: LoadConfigOptions;
7377
config?: GraphQLConfig;
7478
}): Promise<GraphQLCache> {
@@ -98,7 +102,7 @@ export class GraphQLCache implements GraphQLCacheInterface {
98102
_fragmentDefinitionsCache: Map<Uri, Map<string, FragmentInfo>>;
99103
_typeDefinitionsCache: Map<Uri, Map<string, ObjectTypeInfo>>;
100104
_parser: typeof parseDocument;
101-
_logger: Logger;
105+
_logger: Logger | NoopLogger;
102106

103107
constructor({
104108
configDir,
@@ -109,7 +113,7 @@ export class GraphQLCache implements GraphQLCacheInterface {
109113
configDir: Uri;
110114
config: GraphQLConfig;
111115
parser: typeof parseDocument;
112-
logger: Logger;
116+
logger: Logger | NoopLogger;
113117
}) {
114118
this._configDir = configDir;
115119
this._graphQLConfig = config;
@@ -827,7 +831,13 @@ export class GraphQLCache implements GraphQLCacheInterface {
827831
let queries: CachedContent[] = [];
828832
if (content.trim().length !== 0) {
829833
try {
830-
queries = this._parser(content, filePath);
834+
queries = this._parser(
835+
content,
836+
filePath,
837+
DEFAULT_SUPPORTED_EXTENSIONS,
838+
DEFAULT_SUPPORTED_GRAPHQL_EXTENSIONS,
839+
this._logger,
840+
);
831841
if (queries.length === 0) {
832842
// still resolve with an empty ast
833843
return {

packages/graphql-language-service-server/src/MessageProcessor.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,12 @@ import type {
5353
WorkspaceSymbolParams,
5454
Connection,
5555
DidChangeConfigurationRegistrationOptions,
56-
Logger,
5756
} from 'vscode-languageserver/node';
5857

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

6160
import { getGraphQLCache, GraphQLCache } from './GraphQLCache';
62-
import { parseDocument, DEFAULT_SUPPORTED_EXTENSIONS } from './parseDocument';
61+
import { parseDocument } from './parseDocument';
6362

6463
import { printSchema, visit, parse, FragmentDefinitionNode } from 'graphql';
6564
import { tmpdir } from 'node:os';
@@ -72,6 +71,11 @@ import {
7271
ProjectNotFoundError,
7372
} from 'graphql-config';
7473
import type { LoadConfigOptions } from './types';
74+
import {
75+
DEFAULT_SUPPORTED_EXTENSIONS,
76+
SupportedExtensionsEnum,
77+
} from './constants';
78+
import { NoopLogger, Logger } from './Logger';
7579

7680
const configDocLink =
7781
'https://www.npmjs.com/package/graphql-language-service-server#user-content-graphql-configuration-file';
@@ -93,7 +97,7 @@ export class MessageProcessor {
9397
_isInitialized = false;
9498
_isGraphQLConfigMissing: boolean | null = null;
9599
_willShutdown = false;
96-
_logger: Logger;
100+
_logger: Logger | NoopLogger;
97101
_extensions?: GraphQLExtensionDeclaration[];
98102
_parser: (text: string, uri: string) => CachedContent[];
99103
_tmpDir: string;
@@ -114,8 +118,8 @@ export class MessageProcessor {
114118
tmpDir,
115119
connection,
116120
}: {
117-
logger: Logger;
118-
fileExtensions: string[];
121+
logger: Logger | NoopLogger;
122+
fileExtensions: ReadonlyArray<SupportedExtensionsEnum>;
119123
graphqlFileExtensions: string[];
120124
loadConfigOptions: LoadConfigOptions;
121125
config?: GraphQLConfig;
@@ -788,7 +792,7 @@ export class MessageProcessor {
788792
if (parentRange && res.name) {
789793
const isInline = inlineFragments.includes(res.name);
790794
const isEmbedded = DEFAULT_SUPPORTED_EXTENSIONS.includes(
791-
path.extname(textDocument.uri),
795+
path.extname(textDocument.uri) as SupportedExtensionsEnum,
792796
);
793797
if (isInline && isEmbedded) {
794798
const vOffset = parentRange.start.line;

packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ describe('MessageProcessor', () => {
3535
// @ts-ignore
3636
connection: {},
3737
logger,
38-
fileExtensions: ['js'],
3938
graphqlFileExtensions: ['graphql'],
4039
loadConfigOptions: { rootDir: __dirname },
4140
});
@@ -56,6 +55,7 @@ describe('MessageProcessor', () => {
5655
configDir: __dirname,
5756
config: gqlConfig,
5857
parser: parseDocument,
58+
logger: new NoopLogger(),
5959
});
6060
messageProcessor._languageService = {
6161
// @ts-ignore
@@ -484,6 +484,7 @@ export function Example(arg: string) {
484484
}`;
485485

486486
const contents = parseDocument(text, 'test.tsx');
487+
487488
expect(contents[0].query).toEqual(`
488489
query Test {
489490
test {

packages/graphql-language-service-server/src/__tests__/findGraphQLTags-test.ts

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,17 @@
77
*
88
*/
99

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

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

1415
import { NoopLogger } from '../Logger';
16+
import { SupportedExtensionsEnum } from '../constants';
1517

1618
describe('findGraphQLTags', () => {
1719
const logger = new NoopLogger();
18-
const findGraphQLTags = (text: string, ext: string) =>
20+
const findGraphQLTags = (text: string, ext: SupportedExtensionsEnum) =>
1921
baseFindGraphQLTags(text, ext, '', logger);
2022

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

316318
it('finds queries in tagged templates in Svelte using normal <script>', async () => {
317319
const text = `
318-
<script>
319-
gql\`
320-
query {id}
321-
\`;
320+
<script context="module">
321+
const query = graphql(\`
322+
query AllCharacters {
323+
characters {
324+
results {
325+
name
326+
id
327+
image
328+
}
329+
}
330+
}
331+
\`)
332+
export async function load({fetch}) {
333+
return {
334+
props: {
335+
_data: await fetch({
336+
text: query
337+
})
338+
}
339+
}
340+
}
341+
322342
</script>
323343
`;
324344
const contents = findGraphQLTags(text, '.svelte');
325345
expect(contents[0].template).toEqual(`
326-
query {id}`);
346+
query AllCharacters {
347+
characters {
348+
results {
349+
name
350+
id
351+
image
352+
}
353+
}
354+
}
355+
`);
356+
357+
expect(JSON.stringify(contents[0].range)).toEqual(
358+
JSON.stringify(new Range(new Position(2, 29), new Position(12, 0))),
359+
);
327360
});
328361

329362
it('no crash in Svelte files without <script>', async () => {
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import type { ParserOptions, ParserPlugin } from '@babel/parser';
2+
// Attempt to be as inclusive as possible of source text.
3+
export const PARSER_OPTIONS: ParserOptions = {
4+
allowImportExportEverywhere: true,
5+
allowReturnOutsideFunction: true,
6+
allowSuperOutsideMethod: true,
7+
allowAwaitOutsideFunction: true,
8+
// important! this allows babel to keep parsing when there are issues
9+
errorRecovery: true,
10+
sourceType: 'module',
11+
strictMode: false,
12+
};
13+
14+
/**
15+
* .graphql is the officially recommended extension for graphql files
16+
*
17+
* .gql and .graphqls are included for compatibility for commonly used extensions
18+
*
19+
* GQL is a registered trademark of Google, and refers to Google Query Language.
20+
* GraphQL Foundation does *not* recommend using this extension or acronym for
21+
* referring to GraphQL.
22+
*
23+
* any changes should also be reflected in vscode-graphql-syntax textmate grammar & package.json
24+
*/
25+
export const DEFAULT_SUPPORTED_GRAPHQL_EXTENSIONS = [
26+
'.graphql',
27+
'.graphqls',
28+
'.gql',
29+
];
30+
31+
/**
32+
* default tag delimiters to use when parsing GraphQL strings (for js/ts/vue/svelte)
33+
* any changes should also be reflected in vscode-graphql-syntax textmate grammar
34+
*/
35+
export const TAG_MAP: Record<string, true> = {
36+
graphql: true,
37+
gql: true,
38+
graphqls: true,
39+
};
40+
41+
/**
42+
* default extensions to use when parsing for GraphQL strings
43+
* any changes should also be reflected in vscode-graphql-syntax textmate grammar & package.json
44+
*/
45+
export const DEFAULT_SUPPORTED_EXTENSIONS = [
46+
'.js',
47+
'.cjs',
48+
'.mjs',
49+
'.es',
50+
'.esm',
51+
'.es6',
52+
'.ts',
53+
'.jsx',
54+
'.tsx',
55+
'.vue',
56+
'.svelte',
57+
'.cts',
58+
'.mts',
59+
] as const;
60+
export type SupportedExtensions = typeof DEFAULT_SUPPORTED_EXTENSIONS;
61+
export type SupportedExtensionsEnum =
62+
(typeof DEFAULT_SUPPORTED_EXTENSIONS)[number];
63+
64+
/**
65+
* default plugins to use with babel parser
66+
*/
67+
export const BABEL_PLUGINS: ParserPlugin[] = [
68+
'asyncDoExpressions',
69+
'asyncGenerators',
70+
'bigInt',
71+
'classProperties',
72+
'classPrivateProperties',
73+
'classPrivateMethods',
74+
'classStaticBlock',
75+
'doExpressions',
76+
'decimal',
77+
'decorators-legacy',
78+
'destructuringPrivate',
79+
'dynamicImport',
80+
'exportDefaultFrom',
81+
'exportNamespaceFrom',
82+
'functionBind',
83+
'functionSent',
84+
'importMeta',
85+
'importAssertions',
86+
'jsx',
87+
'logicalAssignment',
88+
'moduleBlocks',
89+
'moduleStringNames',
90+
'nullishCoalescingOperator',
91+
'numericSeparator',
92+
'objectRestSpread',
93+
'optionalCatchBinding',
94+
'optionalChaining',
95+
// ['pipelineOperator', { proposal: 'hack' }],
96+
'privateIn',
97+
'regexpUnicodeSets',
98+
'throwExpressions',
99+
'topLevelAwait',
100+
];

0 commit comments

Comments
 (0)