Skip to content

Commit

Permalink
Expose generated types using global module (#31)
Browse files Browse the repository at this point in the history
* expose generated types using global module
fix multi-projects types name conflicts
fix operations name conflicts
expose UnionToArray util type
update README

* upgrade to 1.4.0
  • Loading branch information
Chnapy committed Aug 29, 2022
1 parent 05c4cfd commit 75413b6
Show file tree
Hide file tree
Showing 29 changed files with 398 additions and 288 deletions.
87 changes: 71 additions & 16 deletions README.md
Expand Up @@ -35,7 +35,7 @@ Then add plugin to your `tsconfig.json`
}
```

Since this plugin use [graphql-config](https://www.graphql-config.com/docs/user/user-introduction) you should add a config file targeting your GraphQL schema.
Since this plugin uses [graphql-config](https://www.graphql-config.com/docs/user/user-introduction) you should add a config file targeting your GraphQL schema.

```jsonc
// .graphqlrc
Expand Down Expand Up @@ -78,21 +78,15 @@ gql(`
## Configuration

Configuration can be done at 2 levels: in tsconfig.json and in graphql-config file.
Configuration can be done at 2 levels: in `tsconfig.json` and in `graphql-config` file.

### tsconfig.json

| Property | Description |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| graphqlConfigPath | Optional. Path to GraphQL config file. By default `graphql-config` will lookup to current directory [multiple file naming](https://www.graphql-config.com/docs/user/user-usage#config-search-places). |
| logLevel | Optional. Plugin log level. Values `'default'` - `'verbose'` - `'debug'`. Default `'default'`. |
| projectNameRegex | Optional. For multi-projects GraphQL config, regex for extracting project name from operation. |
Checkout config type & default values in [plugin-config.ts](./src/plugin-config.ts).

> Log level `'debug'` writes log files into `ts-gql-plugin-logs` directory. When running by VSCode this directory can be hard to find, checkout TSServer logs where files paths are logged.
> These logs contain updated code with hidden types generated by plugin.
> Checkout config type in [plugin-config.ts](./src/plugin-config.ts).
### graphql-config

You can add project-related configuration using extension `"ts-gql"`.
Expand All @@ -114,11 +108,7 @@ You can add project-related configuration using extension `"ts-gql"`.
}
```

| Property | Description |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| codegenConfig | Optional. [graphql-codegen](https://www.graphql-code-generator.com/) configuration, using plugins [typescript](https://www.graphql-code-generator.com/plugins/typescript/typescript#config-api-reference) and [typescript-operations](https://www.graphql-code-generator.com/plugins/typescript/typescript-operations#config-api-reference) configuration. |

> Checkout config type in [extension-config.ts](./src/extension-config.ts).
Checkout config type in [extension-config.ts](./src/extension-config.ts).

### Multi-projects configuration

Expand Down Expand Up @@ -180,6 +170,69 @@ gql(`

With this kind of configuration, each of these operations match corresponding project, so its own schema.

## Use of generated types

Even if this plugin allows you to avoid code generation, you may want to use generated types.
For this kind of use a global module is exposed. Named `TsGql`, you can access from it every generated types.

```ts
gql(`
query ProfileAuth {
...
}
`);

const authInput: TsGql.ProfileAuthInput = {
username,
password,
};
```

To use `TsGql` in a file without `gql` uses, you should put a `@ts-gql` tag with the project name you want to use, anywhere in your file.
This is the only way for `ts-gql-plugin` to know without performance impact when you want to access generated types.

```ts
// @ts-gql Profile

const authInput: TsGql.ProfileAuthInput = {
username,
password,
};
```

### Enums

Since enums persist on runtime, they cannot be exposed by `ts-gql-plugin`. To solve this issue, types are generated instead of enums.

```gql
# schema-profile.graphql

enum OAuthProvider {
GOOGLE
FACEBOOK
}
```

So this enum can be used like that:

```ts
// @ts-gql Profile

const provider: TsGql.ProfileOAuthProvider = 'GOOGLE';
```

Also you may want to list every possible values from a GraphQL enum, like to be used with HTML `<select>` elements.
To handle this case `ts-gql-plugin` exposes an utility type, `UnionToArray`, which allows to create a tuple from an union with a strong constraint forcing to give every possible values.

```ts
import { UnionToArray } from 'ts-gql-plugin';

const providerList: UnionToArray<TsGql.ProfileOAuthProvider> = [
'GOOGLE',
'FACEBOOK',
];
```

## VSCode

You should [set your workspace's version of TypeScript](https://code.visualstudio.com/docs/typescript/typescript-compiling#_using-the-workspace-version-of-typescript), which will load plugins from your tsconfig.json file.
Expand All @@ -191,7 +244,7 @@ You should [set your workspace's version of TypeScript](https://code.visualstudi
> Use Workspace Version
```

You also have to restart TS server after **any config change**.
After a config change you may have to restart TS server.

```bash
> TypeScript: Restart TS server
Expand All @@ -213,6 +266,8 @@ Then search for `ts-gql-plugin` occurences.

To have highlighting between other features, you can use [GraphQL extension](https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql) for VSCode.

> Since this extension does not handle multi-projects configurations you may want to use [GraphQL: Syntax Highlighting extension](https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql-syntax) instead. This extension only gives syntax highlighting.
## CLI

Because of [Language Service design limitations](https://github.com/microsoft/TypeScript/wiki/Writing-a-Language-Service-Plugin#whats-a-language-service-plugin) `tsc` does not load plugins. So building or type-checking your files using CLI cannot use `ts-gql-plugin`.
Expand All @@ -232,7 +287,7 @@ 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. This lib can cause issue in your script, so consider wrap its execution into `process.nextTick()`
- 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
4 changes: 2 additions & 2 deletions example/.graphqlrc
Expand Up @@ -2,10 +2,10 @@
"schema": "./schema.graphql",
"projects": {
"Catalog": {
"schema": "./schema.graphql"
"schema": "./schema-catalog.graphql"
},
"Profile": {
"schema": "./schema.graphql"
"schema": "./schema-profile.graphql"
}
}
}
2 changes: 1 addition & 1 deletion example/index.ts
Expand Up @@ -7,7 +7,7 @@ const { data: abc } = useQuery(
user(id: $id) {
id
name
}
}
users {
id
}
Expand Down
1 change: 1 addition & 0 deletions example/schema.graphql → example/schema-catalog.graphql
Expand Up @@ -9,6 +9,7 @@ type User {

enum OAuthProvider {
GOOGLE
FACEBOOK
}

type Auth {
Expand Down
31 changes: 31 additions & 0 deletions example/schema-profile.graphql
@@ -0,0 +1,31 @@
type User {
id: ID!
name: String!
}

enum OAuthProvider {
GOOGLE
FACEBOOK
}

type Auth {
id: ID!
accessToken: String!
user: User!
}

type Query {
users: [User!]!
user(id: ID!): User!
authByGoogle(code: String!): Auth!
}

type Mutation {
updateUser(updateUserInput: UpdateUserInput!): User!
}

input UpdateUserInput {
id: Int!
name: String!
picture: String
}
11 changes: 11 additions & 0 deletions example/using-ts-gql-tags.ts
@@ -0,0 +1,11 @@
// @ts-gql Catalog
// @ts-gql Profile

import { UnionToArray } from 'ts-gql-plugin';

export const provider: TsGql.CatalogOAuthProvider = 'FACEBOOK';

export const providerList: UnionToArray<TsGql.CatalogOAuthProvider> = [
'GOOGLE',
'FACEBOOK',
];
9 changes: 4 additions & 5 deletions package.json
@@ -1,12 +1,11 @@
{
"name": "ts-gql-plugin",
"version": "1.3.5",
"version": "1.4.0",
"packageManager": "yarn@3.2.1",
"license": "MIT",
"main": "./dist/plugin.js",
"main": "./dist/index.js",
"exports": {
".": "./dist/plugin.js",
"./tools": "./dist/tools.js"
".": "./dist/index.js"
},
"files": [
"dist"
Expand All @@ -29,7 +28,7 @@
"url": "https://github.com/chnapy/ts-gql-plugin/issues"
},
"scripts": {
"c:format": "prettier -w .",
"c:format:fix": "prettier -w .",
"c:lint": "eslint . --cache --cache-location node_modules/.cache/.eslintcache",
"c:lint:fix": "yarn c:lint --fix",
"c:type": "tsc-ls -b",
Expand Down
2 changes: 1 addition & 1 deletion src/cached/cached-literal-parser.ts
Expand Up @@ -14,7 +14,7 @@ type CreateCachedLiteralParserOptions = {
export type CachedLiteralParserValue<D extends DocumentInfos = DocumentInfos> =
{
documentInfos: D;
staticGlobals: string;
staticGlobals: string[];
} | null;

type CachedLiteralParserInput = {
Expand Down
3 changes: 2 additions & 1 deletion src/cached/cached-schema-loader.ts
Expand Up @@ -15,7 +15,7 @@ type CreateCachedSchemaLoaderOptions = {
type ProjectInfos = {
schemaFilePath?: string;
schemaDocument: DocumentNode;
staticGlobals: string;
staticGlobals: string[];
extension: ExtensionConfig;
};

Expand Down Expand Up @@ -61,6 +61,7 @@ export const createCachedSchemaLoader = ({
schemaDocument,
staticGlobals: await generateTypeFromSchema(
schemaDocument,
projectName === defaultProjectName ? undefined : projectName,
extension.codegenConfig
),
extension,
Expand Down
2 changes: 1 addition & 1 deletion src/create-error-catcher.ts
@@ -1,5 +1,5 @@
import ts from 'typescript';
import { Logger } from './tools';
import { Logger } from './utils/logger';
import { isVSCodeEnv } from './utils/is-vscode-env';

export type ErrorCatcher = (
Expand Down
4 changes: 3 additions & 1 deletion src/extension-config.ts
Expand Up @@ -3,7 +3,9 @@ import { TypeScriptDocumentsPluginConfig } from '@graphql-codegen/typescript-ope

export type ExtensionConfig = {
/**
* Codegen config for code generation.
* [`graphql-codegen`](https://www.graphql-code-generator.com/) configuration,
* using plugins [typescript](https://www.graphql-code-generator.com/plugins/typescript/typescript#config-api-reference)
* and [typescript-operations](https://www.graphql-code-generator.com/plugins/typescript/typescript-operations#config-api-reference) configuration.
*/
codegenConfig?: TypeScriptPluginConfig & TypeScriptDocumentsPluginConfig;
};
Expand Down
37 changes: 16 additions & 21 deletions src/generators/generate-bottom-content.ts
@@ -1,40 +1,30 @@
import { createUniqueString } from '../utils/create-unique-string';

export type DocumentInfos = {
variables: string;
result: string;
staticTypes: string;
variablesType: string;
operationType: string;
};

export type DocumentInfosWithLiteral = DocumentInfos & {
literal: string;
};

const moduleName = 'TsGql';

export const generateBottomContent = (
documentInfosList: DocumentInfosWithLiteral[],
staticCode: string
) => {
const documentMapContent = documentInfosList
.map(
({ literal, variables, result }) =>
`[\`${literal}\`]: TypedDocumentNode<${result}, ${variables}>;`
({ literal, variablesType, operationType }) =>
`[\`${literal}\`]: TypedDocumentNode<${operationType}, ${variablesType}>;`
)
.join('\n');

const documentStaticTypes = documentInfosList
.map(({ staticTypes }) => staticTypes)
.join('\n');

const moduleName = createUniqueString();

return `
/* eslint-disable */
declare module 'graphql-tag' {
const module = `
module ${moduleName} {
type DocumentNode = import('graphql').DocumentNode;
interface TypedDocumentNode<
export interface TypedDocumentNode<
Result = { [key: string]: unknown },
Variables = { [key: string]: unknown }
> extends DocumentNode {
Expand All @@ -46,15 +36,20 @@ declare module 'graphql-tag' {
__apiType?: (variables: Variables) => Result;
}
interface DocumentMap {
export interface DocumentMap {
${documentMapContent}
}
${staticCode}
${documentStaticTypes}
}
`;

return `
/* eslint-disable */
${module}
declare module 'graphql-tag' {
export function gql<Literal extends keyof ${moduleName}.DocumentMap>(
literals: Literal
): ${moduleName}.DocumentMap[Literal];
Expand Down
25 changes: 11 additions & 14 deletions src/generators/generate-type-from-literal.test.ts
@@ -1,6 +1,6 @@
import { parse } from 'graphql';
import { generateTypeFromLiteral } from './generate-type-from-literal';
import { formatTS } from '../utils/test-utils';
import { formatSpaces } from '../utils/test-utils';

describe('Generate type from literal', () => {
it('generates type from correct string', async () => {
Expand Down Expand Up @@ -32,22 +32,19 @@ describe('Generate type from literal', () => {
}
`;

const expectedVariables = `UserQueryVariables`;
const expectedVariableType = `Exact<{
id: Scalars['ID'];
}>`;

const expectedResult = `UserQueryOperation`;

const expectedStaticTypes = formatTS(`
type UserQueryVariables = Exact<{
id: Scalars['ID'];
}>;
type UserQueryOperation = { __typename?: 'Query', user: { __typename?: 'User', id: string, name: string }, users: Array<{ __typename?: 'User', id: string, email: string }> };
`);
const expectedOperationType = `{ __typename?: 'Query', user: { __typename?: 'User', id: string, name: string }, users: Array<{ __typename?: 'User', id: string, email: string }> }`;

const result = await generateTypeFromLiteral(code, schema);

expect(result.variables).toEqual(expectedVariables);
expect(result.result).toEqual(expectedResult);
expect(formatTS(result.staticTypes)).toEqual(expectedStaticTypes);
expect(formatSpaces(result.variablesType)).toEqual(
formatSpaces(expectedVariableType)
);
expect(formatSpaces(result.operationType)).toEqual(
formatSpaces(expectedOperationType)
);
});
});

1 comment on commit 75413b6

@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: 75413b6 Previous: 05c4cfd Ratio
performance impact %: "with ts-gql-plugin" vs "without ts-gql-plugin" 22.93 % (±1.07%) 23.01 % (±6.90%) 1.00

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

Please sign in to comment.