Skip to content

Commit

Permalink
Add complete cache system for loaders & parser (#28)
Browse files Browse the repository at this point in the history
* add complete cache system for loaders & parser

* enable benchmark dry-run on PRs

* refactor files organization

* add cache-system tests

* upgrade to 1.3.4
  • Loading branch information
Chnapy committed Aug 24, 2022
1 parent 04cdc54 commit 710e298
Show file tree
Hide file tree
Showing 17 changed files with 681 additions and 135 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/benchmark.yml
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches:
- master
pull_request:

permissions:
contents: write
Expand Down Expand Up @@ -33,7 +34,7 @@ jobs:
tool: 'benchmarkjs'
output-file-path: output.txt
github-token: ${{ secrets.GITHUB_TOKEN }}
auto-push: true
auto-push: ${{ github.ref == 'refs/heads/master' }}
alert-threshold: '130%'
comment-always: true
comment-on-alert: true
Expand Down
2 changes: 1 addition & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "ts-gql-plugin",
"version": "1.3.3",
"version": "1.3.4",
"packageManager": "yarn@3.2.1",
"license": "MIT",
"main": "./dist/plugin.js",
Expand Down
70 changes: 70 additions & 0 deletions src/cached/cached-graphql-config-loader.ts
@@ -0,0 +1,70 @@
import { GraphQLProjectConfig, loadConfig } from 'graphql-config';
import { tsGqlExtension } from '../source-update/extension';
import { checkFileLastUpdate, createCacheSystem } from '../utils/cache-system';
import { Logger } from '../utils/logger';

type CreateCachedGraphQLConfigLoaderOptions = {
directory: string;
graphqlConfigPath?: string;
projectNameRegex: string | undefined;
logger: Logger;
};

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

export type CachedGraphQLConfigLoader = ReturnType<
typeof createCachedGraphQLConfigLoader
>;

export const defaultProjectName = 'default';

export const createCachedGraphQLConfigLoader = ({
directory,
graphqlConfigPath,
projectNameRegex,
logger,
}: CreateCachedGraphQLConfigLoaderOptions) =>
createCacheSystem<CachedGraphQLConfigLoaderValue, null>({
// TODO debounce
// debounceValue: 5000,
getKeyFromInput: () => '',
create: async () => {
const graphqlConfig = await loadConfig({
rootDir: directory,
filepath: graphqlConfigPath,
throwOnMissing: true,
throwOnEmpty: true,
extensions: [tsGqlExtension],
});
if (!graphqlConfig) {
throw new Error('GraphQL config file not found.');
}

const graphqlProjectsMap = graphqlConfig.projects;

if (!(defaultProjectName in graphqlProjectsMap) && !projectNameRegex) {
throw new Error(
'Multiple projects into GraphQL config. You must define projectNameRegex in config.'
);
}

const graphqlProjects = Object.values(graphqlProjectsMap);

logger.log(`GraphQL config loaded from ${graphqlConfig.filepath}`);

graphqlProjects.forEach(({ name, schema }) =>
logger.log(`GraphQL project "${name}" schema loaded from ${schema}`)
);

return { configFilePath: graphqlConfig.filepath, graphqlProjects };
},
checkValidity: async (currentItem) => {
const { configFilePath } = await currentItem.value;

return checkFileLastUpdate(configFilePath, currentItem.dateTime);
},
sizeLimit: 40,
});
90 changes: 90 additions & 0 deletions src/cached/cached-literal-parser.ts
@@ -0,0 +1,90 @@
import ts from 'typescript/lib/tsserverlibrary';
import { ErrorCatcher } from '../create-error-catcher';
import { generateTypeFromLiteral } from '../generators/generate-type-from-literal';
import { DocumentInfos } from '../generators/generate-bottom-content';
import { createCacheSystem } from '../utils/cache-system';
import { CachedSchemaLoader, defaultProjectName } from './cached-schema-loader';

type CreateCachedLiteralParserOptions = {
cachedSchemaLoader: CachedSchemaLoader;
projectNameRegex: string | undefined;
scriptTarget: ts.ScriptTarget;
errorCatcher: ErrorCatcher;
};

export type CachedLiteralParserValue<D extends DocumentInfos = DocumentInfos> =
{
documentInfos: D;
staticGlobals: string;
} | null;

type CachedLiteralParserInput = {
literal: string;
filename: string;
initialSource: string;
};

export type CachedLiteralParser = ReturnType<typeof createCachedLiteralParser>;

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

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

const project = await cachedSchemaLoader.getItemOrCreate({
projectName,
});
if (!project) {
throw new Error(`Project not defined for name "${projectName}"`);
}

return project;
};

const parser = createCacheSystem<
CachedLiteralParserValue,
CachedLiteralParserInput
>({
getKeyFromInput: (input) => input.literal.replaceAll(/\s/gi, ''),
create: async ({ literal, filename, initialSource }) => {
try {
const project = await getProjectFromLiteral(literal);

return {
documentInfos: await generateTypeFromLiteral(
literal,
project.schemaDocument,
project.extension.codegenConfig
),
staticGlobals: project.staticGlobals,
};
} catch (error) {
errorCatcher(
error,
ts.createSourceFile(filename, initialSource, scriptTarget),
initialSource.indexOf(literal),
literal.length
);
return null;
}
},
checkValidity: async ({ input }) => {
const projectName = getProjectNameFromLiteral(input.literal);

return await cachedSchemaLoader.checkItemValidity({
projectName,
});
},
});

return parser;
};
90 changes: 90 additions & 0 deletions src/cached/cached-schema-loader.ts
@@ -0,0 +1,90 @@
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 { checkFileLastUpdate, createCacheSystem } from '../utils/cache-system';
import { CachedGraphQLConfigLoader } from './cached-graphql-config-loader';

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

type ProjectInfos = {
schemaFilePath?: string;
schemaDocument: DocumentNode;
staticGlobals: string;
extension: ExtensionConfig;
};

type CachedSchemaLoaderValue = ProjectInfos | null;

type CachedSchemaLoaderInput = {
projectName: string;
};

export type CachedSchemaLoader = ReturnType<typeof createCachedSchemaLoader>;

export const defaultProjectName = 'default';

export const createCachedSchemaLoader = ({
cachedGraphQLConfigLoader,
errorCatcher,
}: CreateCachedSchemaLoaderOptions) =>
createCacheSystem<CachedSchemaLoaderValue, CachedSchemaLoaderInput>({
// TODO debounce
// debounceValue: 1000,
getKeyFromInput: (input) => input.projectName,
create: async ({ projectName }) => {
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 extension = getProjectExtension(project);

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

return project
.getSchema('DocumentNode')
.then(
async (schemaDocument): Promise<ProjectInfos> => ({
schemaFilePath,
schemaDocument,
staticGlobals: await generateTypeFromSchema(
schemaDocument,
extension.codegenConfig
),
extension,
})
)
.catch(errorCatcher);
},
checkValidity: async (currentItem) => {
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);
},
sizeLimit: 40,
});
@@ -1,14 +1,17 @@
import { createUniqueString } from '../utils/create-unique-string';

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

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

export const generateBottomContent = (
documentInfosList: DocumentInfos[],
documentInfosList: DocumentInfosWithLiteral[],
staticCode: string
) => {
const documentMapContent = documentInfosList
Expand Down
@@ -1,9 +1,9 @@
import { parse } from 'graphql';
import { extractTypeFromLiteral } from './extract-type-from-literal';
import { formatGQL, formatTS } from './test-utils';
import { generateTypeFromLiteral } from './generate-type-from-literal';
import { formatTS } from '../utils/test-utils';

describe('Extract type from literal', () => {
it('extracts type from correct string', async () => {
describe('Generate type from literal', () => {
it('generates type from correct string', async () => {
const schema = parse(`
type User {
id: ID!
Expand Down Expand Up @@ -44,9 +44,8 @@ describe('Extract type from literal', () => {
type UserQueryOperation = { __typename?: 'Query', user: { __typename?: 'User', id: string, name: string }, users: Array<{ __typename?: 'User', id: string, email: string }> };
`);

const result = await extractTypeFromLiteral(code, schema);
const result = await generateTypeFromLiteral(code, schema);

expect(formatGQL(result.literal)).toEqual(formatGQL(code));
expect(result.variables).toEqual(expectedVariables);
expect(result.result).toEqual(expectedResult);
expect(formatTS(result.staticTypes)).toEqual(expectedStaticTypes);
Expand Down
Expand Up @@ -9,7 +9,7 @@ const operationsRegex = /type (\w+Operation) = /s;
type CodegenPlugin = typeof plugins[number];
const plugins = [typescriptOperationsPlugin];

export const extractTypeFromLiteral = async (
export const generateTypeFromLiteral = async (
literal: string,
schema: DocumentNode,
codegenConfig: typescriptOperationsPlugin.TypeScriptDocumentsPluginConfig = {}
Expand Down Expand Up @@ -61,7 +61,6 @@ export const extractTypeFromLiteral = async (
}

return {
literal,
variables,
result,
staticTypes,
Expand Down
@@ -1,9 +1,9 @@
import { parse } from 'graphql';
import { formatTS } from './test-utils';
import { extractTypeFromSchema } from './extract-type-from-schema';
import { formatTS } from '../utils/test-utils';
import { generateTypeFromSchema } from './generate-type-from-schema';

describe('Extract type from schema', () => {
it('extracts type from correct string', async () => {
describe('Generate type from schema', () => {
it('generates type from correct string', async () => {
const schema = parse(`
type User {
id: ID!
Expand Down Expand Up @@ -55,7 +55,7 @@ describe('Extract type from schema', () => {
};
`;

const result = await extractTypeFromSchema(schema);
const result = await generateTypeFromSchema(schema);

expect(formatTS(result)).toEqual(formatTS(expected));
});
Expand Down
Expand Up @@ -5,7 +5,7 @@ import { DocumentNode } from 'graphql';
type CodegenPlugin = typeof plugins[number];
const plugins = [typescriptPlugin];

export const extractTypeFromSchema = async (
export const generateTypeFromSchema = async (
schema: DocumentNode,
codegenConfig: typescriptPlugin.TypeScriptPluginConfig = {}
): Promise<string> => {
Expand Down
2 changes: 1 addition & 1 deletion src/source-update/create-source-updater.test.ts
Expand Up @@ -3,7 +3,7 @@
import * as createUniqueStringExport from '../utils/create-unique-string';
import { Logger } from '../utils/logger';
import { createSourceUpdater } from './create-source-updater';
import { formatTS } from './test-utils';
import { formatTS } from '../utils/test-utils';
import { join } from 'node:path';

const resolveTestFile = (path: string) => join('src/test-files', path);
Expand Down

1 comment on commit 710e298

@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: 710e298 Previous: 04cdc54 Ratio
performance impact %: "with ts-gql-plugin" vs "without ts-gql-plugin" 18.82 % (±5.57%) 19.39 % (±7.66%) 1.03

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

Please sign in to comment.