Skip to content

Commit

Permalink
Add gql editor support: autocomplete / quick infos / go to definitions (
Browse files Browse the repository at this point in the history
#40)

* add gql quick infos

* add quick-infos test

* add gql auto-complete with test

* add gql go to definitions

* improve get definition and bound span
add tests

* improve definition coverage

* update README

* upgrade to 1.5.0
  • Loading branch information
Chnapy authored Sep 22, 2022
1 parent cc394b5 commit efa6c5c
Show file tree
Hide file tree
Showing 56 changed files with 1,997 additions and 518 deletions.
Binary file modified .github/images/example.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
485 changes: 316 additions & 169 deletions .pnp.cjs

Large diffs are not rendered by default.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ A [TypeScript Language Service Plugin](https://github.com/Microsoft/TypeScript/w

<img src="https://raw.githubusercontent.com/chnapy/ts-gql-plugin/master/.github/images/example.gif" alt="ts-gql-plugin example" />

---

- :triangular_ruler: Typed GraphQL operations
- :x: No code generation
- :toolbox: [CLI support](#cli)
- :pencil: Editor support with autocomplete / quick-infos / "go to definition"
- :link: Multi-projects support

---

Using `gql` from `graphql-tag` gives you generic `DocumentNode` type, which does not allow you to manipulate typed requested data when used with Apollo for example. To resolve that you can use [code generators](https://www.graphql-code-generator.com/) creating typescript code with correct types, but it adds lot of generated code with risk of obsolete code and bad development comfort.

`ts-gql-plugin` is meant to solve this issue, by replacing most of code generation by compiler-side typing, using [TypeScript Language Service Plugin](https://github.com/Microsoft/TypeScript/wiki/Writing-a-Language-Service-Plugin).
Expand Down Expand Up @@ -287,7 +297,6 @@ gql`

- since Language Service feature is limited concerning types overriding, solution was to parse & override text source files during TS server process, which is subobtimal for performances (best solution would have been to work with AST)
- as described upper, CLI is not handled out-of-box because of `tsc` design limitations
- because TypeScript compiler does not handle async operations, required by some dependencies, so use of [`deasync`](https://github.com/abbr/deasync) is required

## Benchmark

Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ts-gql-plugin",
"version": "1.4.2",
"version": "1.5.0",
"packageManager": "yarn@3.2.1",
"license": "MIT",
"main": "./dist/index.js",
Expand Down Expand Up @@ -43,7 +43,9 @@
"@graphql-codegen/typescript-operations": "2.3.7",
"@graphql-tools/graphql-tag-pluck": "7.3.3",
"deasync": "0.1.26",
"graphql-config": "4.3.1"
"graphql-config": "4.3.5",
"graphql-language-service": "5.0.6",
"graphql-language-service-utils": "2.7.1"
},
"peerDependencies": {
"graphql": ">= 16",
Expand Down
124 changes: 124 additions & 0 deletions src/cached/cached-document-schema-loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { DocumentNode } from 'graphql';
import path from 'node:path';
import { ErrorCatcher } from '../create-error-catcher';
import { ExtensionConfig } from '../extension-config';
import { generateTypeFromSchema } from '../generators/generate-type-from-schema';
import { getProjectExtension } from '../source-update/extension';
import {
CacheItem,
checkFileLastUpdate,
createCacheSystem,
} from '../utils/cache-system';
import { CachedGraphQLConfigLoader } from './cached-graphql-config-loader';

export type CreateCachedSchemaLoaderOptions = {
cachedGraphQLConfigLoader: CachedGraphQLConfigLoader;
errorCatcher: ErrorCatcher;
};

export type SchemaProjectInfos<D> = {
schemaFilePath?: string;
schemaDocument: D;
};

type ProjectInfos = SchemaProjectInfos<DocumentNode> & {
staticGlobals: string[];
extension: ExtensionConfig;
};

type CachedDocumentSchemaLoaderValue = ProjectInfos | null;

export type CachedSchemaLoaderInput = {
projectName: string;
};

export type CachedDocumentSchemaLoader = ReturnType<
typeof createCachedDocumentSchemaLoader
>;

export const defaultProjectName = 'default';

export const getProjectNameIfNotDefault = (projectName: string) =>
projectName === defaultProjectName ? undefined : projectName;

export const getCreateProjectInfos = async (
cachedGraphQLConfigLoader: CachedGraphQLConfigLoader,
{ projectName }: CachedSchemaLoaderInput
) => {
const { graphqlProjects } = await cachedGraphQLConfigLoader.getItemOrCreate(
null
);

const project = graphqlProjects.find(({ name }) => name === projectName);
if (!project) {
throw new Error(`Project not defined for name "${projectName}"`);
}

const schemaFilePath =
typeof project.schema === 'string'
? path.join(project.dirpath, project.schema)
: undefined;

return {
project,
schemaFilePath,
};
};

export const getCachedSchemaCheckValidity =
(cachedGraphQLConfigLoader: CachedGraphQLConfigLoader) =>
async (
currentItem: CacheItem<SchemaProjectInfos<unknown> | null, unknown>
) => {
const isGraphQLConfigValid =
await cachedGraphQLConfigLoader.checkItemValidity(null);
if (!isGraphQLConfigValid) {
return false;
}

const project = await currentItem.value;
if (!project) {
return true;
}

if (!project.schemaFilePath) {
return false;
}

return checkFileLastUpdate(project.schemaFilePath, currentItem.dateTime);
};

export const createCachedDocumentSchemaLoader = ({
cachedGraphQLConfigLoader,
errorCatcher,
}: CreateCachedSchemaLoaderOptions) =>
createCacheSystem<CachedDocumentSchemaLoaderValue, CachedSchemaLoaderInput>({
// TODO debounce
// debounceValue: 1000,
getKeyFromInput: (input) => input.projectName,
create: async (input) => {
const { project, schemaFilePath } = await getCreateProjectInfos(
cachedGraphQLConfigLoader,
input
);

const extension = getProjectExtension(project);

return project
.getSchema('DocumentNode')
.then(
async (schemaDocument): Promise<ProjectInfos> => ({
schemaFilePath,
schemaDocument,
staticGlobals: await generateTypeFromSchema(
schemaDocument,
getProjectNameIfNotDefault(input.projectName),
extension.codegenConfig
),
extension,
})
)
.catch(errorCatcher);
},
checkValidity: getCachedSchemaCheckValidity(cachedGraphQLConfigLoader),
});
13 changes: 11 additions & 2 deletions src/cached/cached-graphql-config-loader.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { GraphQLProjectConfig, loadConfig } from 'graphql-config';
import {
GraphQLConfig,
GraphQLProjectConfig,
loadConfig,
} from 'graphql-config';
import { tsGqlExtension } from '../source-update/extension';
import { checkFileLastUpdate, createCacheSystem } from '../utils/cache-system';
import { Logger } from '../utils/logger';
Expand All @@ -12,6 +16,7 @@ type CreateCachedGraphQLConfigLoaderOptions = {

type CachedGraphQLConfigLoaderValue = {
configFilePath: string;
graphqlConfig: GraphQLConfig;
graphqlProjects: GraphQLProjectConfig[];
};

Expand Down Expand Up @@ -59,7 +64,11 @@ export const createCachedGraphQLConfigLoader = ({
logger.log(`GraphQL project "${name}" schema loaded from ${schema}`)
);

return { configFilePath: graphqlConfig.filepath, graphqlProjects };
return {
configFilePath: graphqlConfig.filepath,
graphqlConfig,
graphqlProjects,
};
},
checkValidity: async (currentItem) => {
const { configFilePath } = await currentItem.value;
Expand Down
39 changes: 39 additions & 0 deletions src/cached/cached-graphql-schema-loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { GraphQLSchema } from 'graphql';
import { createCacheSystem } from '../utils/cache-system';
import {
CachedSchemaLoaderInput,
CreateCachedSchemaLoaderOptions,
getCachedSchemaCheckValidity,
getCreateProjectInfos,
SchemaProjectInfos,
} from './cached-document-schema-loader';

type CachedSchemaLoaderValue = SchemaProjectInfos<GraphQLSchema> | null;

export type CachedGraphQLSchemaLoader = ReturnType<
typeof createCachedGraphQLSchemaLoader
>;

export const createCachedGraphQLSchemaLoader = ({
cachedGraphQLConfigLoader,
errorCatcher,
}: CreateCachedSchemaLoaderOptions) =>
createCacheSystem<CachedSchemaLoaderValue, CachedSchemaLoaderInput>({
getKeyFromInput: (input) => input.projectName,
create: async (input) => {
const { project, schemaFilePath } = await getCreateProjectInfos(
cachedGraphQLConfigLoader,
input
);

return project
.getSchema('GraphQLSchema')
.then(async (schemaDocument) => ({
schemaFilePath,
schemaDocument,
}))
.catch(errorCatcher);
},
checkValidity: getCachedSchemaCheckValidity(cachedGraphQLConfigLoader),
sizeLimit: 40,
});
32 changes: 19 additions & 13 deletions src/cached/cached-literal-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import { generateTypeFromLiteral } from '../generators/generate-type-from-litera
import { DocumentInfos } from '../generators/generate-bottom-content';
import { createCacheSystem } from '../utils/cache-system';
import {
CachedSchemaLoader,
CachedDocumentSchemaLoader,
defaultProjectName,
getProjectNameIfNotDefault,
} from './cached-schema-loader';
} from './cached-document-schema-loader';

type CreateCachedLiteralParserOptions = {
cachedSchemaLoader: CachedSchemaLoader;
cachedDocumentSchemaLoader: CachedDocumentSchemaLoader;
projectNameRegex: string | undefined;
errorCatcher: ErrorCatcher;
};
Expand All @@ -28,20 +28,23 @@ type CachedLiteralParserInput = {

export type CachedLiteralParser = ReturnType<typeof createCachedLiteralParser>;

export const getProjectNameFromLiteral = (
literal: string,
projectNameRegex: string | undefined
) =>
projectNameRegex
? (new RegExp(projectNameRegex).exec(literal) ?? [])[0]
: defaultProjectName;

export const createCachedLiteralParser = ({
cachedSchemaLoader,
cachedDocumentSchemaLoader,
projectNameRegex,
errorCatcher,
}: CreateCachedLiteralParserOptions) => {
const getProjectNameFromLiteral = (literal: string) =>
projectNameRegex
? (new RegExp(projectNameRegex).exec(literal) ?? [])[0]
: defaultProjectName;

const getProjectFromLiteral = async (literal: string) => {
const projectName = getProjectNameFromLiteral(literal);
const projectName = getProjectNameFromLiteral(literal, projectNameRegex);

const project = await cachedSchemaLoader.getItemOrCreate({
const project = await cachedDocumentSchemaLoader.getItemOrCreate({
projectName,
});
if (!project) {
Expand Down Expand Up @@ -82,9 +85,12 @@ export const createCachedLiteralParser = ({
}
},
checkValidity: async ({ input }) => {
const projectName = getProjectNameFromLiteral(input.literal);
const projectName = getProjectNameFromLiteral(
input.literal,
projectNameRegex
);

return await cachedSchemaLoader.checkItemValidity({
return await cachedDocumentSchemaLoader.checkItemValidity({
projectName,
});
},
Expand Down
94 changes: 0 additions & 94 deletions src/cached/cached-schema-loader.ts

This file was deleted.

Loading

1 comment on commit efa6c5c

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"with ts-gql-plugin" vs "without ts-gql-plugin" Benchmark

Benchmark suite Current: efa6c5c Previous: cc394b5 Ratio
performance impact %: "with ts-gql-plugin" vs "without ts-gql-plugin" 27.57 % (±5.89%) 23.02 % (±1.23%) 0.83

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.