Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Replace TS-based with string-based preprocessed introspection output #154

Merged
merged 7 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ jobs:
run: pnpm run lint

- name: Unit Tests
run: pnpm multi --include-workspace-root run test --run
run: pnpm run test

- name: benchmarks
run: pnpm multi --include-workspace-root run bench --run
run: pnpm run bench --run

- name: Build
run: pnpm multi --include-workspace-root run build
1 change: 1 addition & 0 deletions packages/internal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"homepage": "https://gql-tada.0no.co/",
"license": "MIT",
"scripts": {
"test": "vitest test",
"build": "rollup -c ../../scripts/rollup.config.mjs",
"clean": "rimraf dist node_modules/.cache",
"prepublishOnly": "run-s clean build"
Expand Down
135 changes: 0 additions & 135 deletions packages/internal/src/introspection.ts

This file was deleted.

39 changes: 39 additions & 0 deletions packages/internal/src/introspection/__tests__/preprocess.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, test } from 'vitest';

import * as ts from '../../../../../src/__tests__/tsHarness';
import simpleIntrospection from '../../../../../src/__tests__/fixtures/simpleIntrospection.json';
import { preprocessIntrospection } from '../preprocess';

const testTypeHost = test.each([
{ strictNullChecks: false, noImplicitAny: false },
{ strictNullChecks: true },
]);

describe('preprocessIntrospection', () => {
testTypeHost('matches `mapIntrospection` output (%o)', (options) => {
const introspectionType = preprocessIntrospection(simpleIntrospection as any);

const virtualHost = ts.createVirtualHost({
...ts.readVirtualModule('expect-type'),
'utils.ts': ts.readFileFromRoot('src/utils.ts'),
'introspection.ts': ts.readFileFromRoot('src/introspection.ts'),
'simpleSchema.ts': ts.readFileFromRoot('src/__tests__/fixtures/simpleSchema.ts'),
'output.ts': `export type output = ${introspectionType};`,
'index.ts': `
import { expectTypeOf } from 'expect-type';
import type { simpleSchema } from './simpleSchema';
import type { output } from './output';

expectTypeOf<output>().toMatchTypeOf<simpleSchema>();
`,
});

ts.runDiagnostics(
ts.createTypeHost({
...options,
rootNames: ['index.ts'],
host: virtualHost,
})
);
});
});
2 changes: 2 additions & 0 deletions packages/internal/src/introspection/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { preprocessIntrospection } from './preprocess';
export * from './output';
58 changes: 58 additions & 0 deletions packages/internal/src/introspection/output.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { IntrospectionQuery } from 'graphql';
import { minifyIntrospectionQuery } from '@urql/introspection';

import { TadaError } from '../errors';
import { PREAMBLE_IGNORE, ANNOTATION_DTS, ANNOTATION_TS } from './constants';
import { preprocessIntrospection } from './preprocess';

const stringifyJson = (input: unknown | string): string =>
typeof input === 'string' ? input : JSON.stringify(input, null, 2);

export function minifyIntrospection(introspection: IntrospectionQuery): IntrospectionQuery {
return minifyIntrospectionQuery(introspection, {
includeDirectives: false,
includeEnums: true,
includeInputs: true,
includeScalars: true,
});
}

interface OutputIntrospectionFileOptions {
fileType: '.ts' | '.d.ts' | string;
shouldPreprocess?: boolean;
}

export function outputIntrospectionFile(
introspection: IntrospectionQuery | string,
opts: OutputIntrospectionFileOptions
): string {
if (/\.d\.ts$/.test(opts.fileType)) {
const json =
typeof introspection !== 'string' && opts.shouldPreprocess
? preprocessIntrospection(introspection)
: stringifyJson(introspection);
return [
PREAMBLE_IGNORE,
ANNOTATION_DTS,
`export type introspection = ${json};\n`,
"import * as gqlTada from 'gql.tada';\n",
"declare module 'gql.tada' {",
' interface setupSchema {',
' introspection: introspection',
' }',
'}',
].join('\n');
} else if (/\.ts$/.test(opts.fileType)) {
const json = stringifyJson(introspection);
return [
PREAMBLE_IGNORE,
ANNOTATION_TS,
`const introspection = ${json} as const;\n`,
'export { introspection };',
].join('\n');
}

throw new TadaError(
`No available introspection format for "${opts.fileType}" (expected ".ts" or ".d.ts")`
);
}
102 changes: 102 additions & 0 deletions packages/internal/src/introspection/preprocess.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type {
IntrospectionQuery,
IntrospectionType,
IntrospectionEnumValue,
IntrospectionInputValue,
IntrospectionTypeRef,
IntrospectionNamedTypeRef,
IntrospectionField,
} from 'graphql';

const printName = (input: string | undefined | null): string => (input ? `'${input}'` : 'never');

const printTypeRef = (typeRef: IntrospectionTypeRef) => {
if (typeRef.kind === 'NON_NULL') {
return `{ kind: 'NON_NULL'; name: never; ofType: ${printTypeRef(typeRef.ofType)}; }`;
} else if (typeRef.kind === 'LIST') {
return `{ kind: 'LIST'; name: never; ofType: ${printTypeRef(typeRef.ofType)}; }`;
} else {
return `{ kind: ${printName(typeRef.kind)}; name: ${printName(typeRef.name)}; ofType: null; }`;
}
};

const printInputFields = (inputFields: readonly IntrospectionInputValue[]) => {
let output = '';
for (const inputField of inputFields) {
if (output) output += ', ';
const name = printName(inputField.name);
const type = printTypeRef(inputField.type);
const defaultValue = inputField.defaultValue ? JSON.stringify(inputField.defaultValue) : 'null';
output += `{ name: ${name}; type: ${type}; defaultValue: ${defaultValue} }`;
}
return `[${output}]`;
};

const printNamedTypes = (
values: readonly (IntrospectionEnumValue | IntrospectionNamedTypeRef)[]
) => {
if (!values.length) return 'never';
let output = '';
for (const value of values) {
if (output) output += ' | ';
output += printName(value.name);
}
return output;
};

const printFields = (fields: readonly IntrospectionField[]) => {
let output = '';
for (const field of fields) {
const name = printName(field.name);
const type = printTypeRef(field.type);
output += `${printName(field.name)}: { name: ${name}; type: ${type} }; `;
}
return `{ ${output}}`;
};

export const printIntrospectionType = (type: IntrospectionType) => {
if (type.kind === 'ENUM') {
const values = printNamedTypes(type.enumValues);
return `{ kind: 'ENUM'; name: ${printName(type.name)}; type: ${values}; }`;
} else if (type.kind === 'INPUT_OBJECT') {
const fields = printInputFields(type.inputFields);
return `{ kind: 'INPUT_OBJECT'; name: ${printName(type.name)}; inputFields: ${fields}; }`;
} else if (type.kind === 'OBJECT') {
const fields = printFields(type.fields);
return `{ kind: 'OBJECT'; name: ${printName(type.name)}; fields: ${fields}; }`;
} else if (type.kind === 'INTERFACE') {
const name = printName(type.name);
const fields = printFields(type.fields);
const possibleTypes = printNamedTypes(type.possibleTypes);
return `{ kind: 'INTERFACE'; name: ${name}; fields: ${fields}; possibleTypes: ${possibleTypes}; }`;
} else if (type.kind === 'UNION') {
const name = printName(type.name);
const possibleTypes = printNamedTypes(type.possibleTypes);
return `{ kind: 'UNION'; name: ${name}; fields: {}; possibleTypes: ${possibleTypes}; }`;
} else if (type.kind === 'SCALAR') {
return 'unknown';
} else {
return 'never';
}
};

export function preprocessIntrospection({ __schema: schema }: IntrospectionQuery): string {
const queryName = printName(schema.queryType.name);
const mutationName = printName(schema.mutationType && schema.mutationType.name);
const subscriptionName = printName(schema.subscriptionType && schema.subscriptionType.name);

let evaluatedTypes = '';
for (const type of schema.types) {
const typeStr = printIntrospectionType(type);
if (evaluatedTypes) evaluatedTypes += '\n';
evaluatedTypes += ` ${printName(type.name)}: ${typeStr};`;
}

return (
'{\n' +
` query: ${queryName};\n` +
` mutation: ${mutationName};\n` +
` subscription: ${subscriptionName};\n` +
` types: {\n${evaluatedTypes}\n };\n}`
);
}
Loading
Loading