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

feat: Implement pre-processed outputs and expose output formats in @gql.tada/internal #150

Merged
merged 12 commits into from
Mar 20, 2024
6 changes: 6 additions & 0 deletions .changeset/three-spoons-call.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@gql.tada/cli-utils": minor
"@gql.tada/internal": minor
---

Expose introspection output format generation from `@gql.tada/internal` and implement a new pre-processed output format, which pre-computes the output of the `mapIntrospection` type.
4 changes: 1 addition & 3 deletions packages/cli-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,9 @@
"json5": "^2.2.3",
"rollup": "^4.9.4",
"sade": "^1.8.1",
"type-fest": "^4.10.2",
"typescript": "^5.3.3"
"type-fest": "^4.10.2"
},
"dependencies": {
"@urql/introspection": "^1.0.3",
"@gql.tada/internal": "workspace:*",
"graphql": "^16.8.1"
},
Expand Down
30 changes: 21 additions & 9 deletions packages/cli-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,19 +117,19 @@ export async function generateSchema(
await fs.writeFile(resolve(cwd, destination), printSchema(schema), 'utf-8');
}

export async function generateTadaTypes(cwd: string = process.cwd()) {
export async function generateTadaTypes(shouldPreprocess = false, cwd: string = process.cwd()) {
const tsconfigpath = path.resolve(cwd, 'tsconfig.json');
const hasTsConfig = existsSync(tsconfigpath);
if (!hasTsConfig) {
console.error('Missing tsconfig.json');
return;
}

const root = resolveTypeScriptRootDir(
readFileSync as (path: string) => string | undefined,
tsconfigpath
);
const tsconfigContents = await fs.readFile(root || tsconfigpath, 'utf-8');
// TODO: Remove redundant read and move tsconfig.json handling to internal package
const root =
resolveTypeScriptRootDir(readFileSync as (path: string) => string | undefined, tsconfigpath) ||
cwd;
const tsconfigContents = await fs.readFile(path.resolve(root, 'tsconfig.json'), 'utf-8');
let tsConfig: TsConfigJson;
try {
tsConfig = parse(tsconfigContents) as TsConfigJson;
Expand All @@ -146,7 +146,12 @@ export async function generateTadaTypes(cwd: string = process.cwd()) {
(plugin) => plugin.name === '@0no-co/graphqlsp' || plugin.name === 'gql.tda/cli'
) as GraphQLSPConfig;

await ensureTadaIntrospection(foundPlugin.schema, foundPlugin.tadaOutputLocation!, cwd);
await ensureTadaIntrospection(
foundPlugin.schema,
foundPlugin.tadaOutputLocation!,
cwd,
shouldPreprocess
);
}

const prog = sade('gql.tada');
Expand Down Expand Up @@ -179,16 +184,23 @@ async function main() {
});
}

generateSchema(target, {
return generateSchema(target, {
headers: parsedHeaders,
output: options.output,
});
})
.command('generate-output')
.option(
'--preprocess',
'Enables pre-processing, converting the introspection data to a more efficient schema structure ahead of time'
)
.describe(
'Generate the gql.tada types file, this will look for your "tsconfig.json" and use the "@0no-co/graphqlsp" configuration to generate the file.'
)
.action(() => generateTadaTypes());
.action((options) => {
const shouldPreprocess = !!options.preprocess && options.preprocess !== 'false';
return generateTadaTypes(shouldPreprocess);
});
prog.parse(process.argv);
}

Expand Down
19 changes: 19 additions & 0 deletions packages/cli-utils/src/resolve.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import path from 'node:path';
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { createRequire } from 'node:module';

export const dirname =
typeof __dirname !== 'string' ? path.dirname(fileURLToPath(import.meta.url)) : __dirname;

export const requireResolve =
typeof require === 'function' ? require.resolve : createRequire(import.meta.url).resolve;

export const loadTypings = async () => {
const tadaModule = requireResolve('gql.tada/package.json', {
paths: ['node_modules', ...(requireResolve.paths('gql.tada') || [])],
});

const typingsPath = path.join(path.dirname(tadaModule), 'dist/gql-tada.d.ts');
return readFile(typingsPath, { encoding: 'utf8' });
};
136 changes: 33 additions & 103 deletions packages/cli-utils/src/tada.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,15 @@
import { promises as fs } from 'node:fs';
import path from 'node:path';
import type { GraphQLSchema, IntrospectionQuery } from 'graphql';

import {
buildClientSchema,
buildSchema,
getIntrospectionQuery,
introspectionFromSchema,
} from 'graphql';
import { minifyIntrospectionQuery } from '@urql/introspection';

export const tadaGqlContents = `import { initGraphQLTada } from 'gql.tada';
import type { introspection } from './introspection';

export const graphql = initGraphQLTada<{
introspection: typeof introspection;
}>();

export type { FragmentOf, ResultOf, VariablesOf } from 'gql.tada';
export type { FragmentOf as FragmentType } from 'gql.tada';
export { readFragment } from 'gql.tada';
export { readFragment as useFragment } from 'gql.tada';
`;
import { minifyIntrospection, outputIntrospectionFile } from '@gql.tada/internal';

/**
* This function mimics the behavior of the LSP, this so we can ensure
Expand All @@ -30,72 +19,50 @@ export { readFragment as useFragment } from 'gql.tada';
* this function.
*/
export async function ensureTadaIntrospection(
schemaLocation: SchemaOrigin | string,
schemaLocation: SchemaOrigin,
outputLocation: string,
base: string = process.cwd()
base: string = process.cwd(),
shouldPreprocess = true
) {
const writeTada = async () => {
try {
const schema = await loadSchema(base, schemaLocation);
if (!schema) {
console.error('Something went wrong while trying to load the schema.');
return;
}
const introspection = introspectionFromSchema(schema, {
descriptions: false,
});
const minified = minifyIntrospectionQuery(introspection, {
includeDirectives: false,
includeEnums: true,
includeInputs: true,
includeScalars: true,
});
const schema = await loadSchema(base, schemaLocation);
if (!schema) {
console.error('Something went wrong while trying to load the schema.');
return;
}

const json = JSON.stringify(minified, null, 2);
const resolvedOutputLocation = path.resolve(base, outputLocation);
let contents;
const introspection = minifyIntrospection(
introspectionFromSchema(schema, {
descriptions: false,
})
);

if (/\.d\.ts$/.test(outputLocation)) {
contents = [
preambleComments,
dtsAnnotationComment,
`export type introspection = ${json};\n`,
"import * as gqlTada from 'gql.tada';\n",
"declare module 'gql.tada' {",
' interface setupSchema {',
' introspection: introspection',
' }',
'}',
].join('\n');
} else if (path.extname(outputLocation) === '.ts') {
contents = [
preambleComments,
tsAnnotationComment,
`const introspection = ${json} as const;\n`,
'export { introspection };',
].join('\n');
} else {
console.warn('Invalid file extension for tadaOutputLocation.');
return;
}
const contents = await outputIntrospectionFile(introspection, {
fileType: outputLocation,
shouldPreprocess,
});

await fs.writeFile(resolvedOutputLocation, contents);
} catch (e) {
console.error('Something went wrong while writing the introspection file', e);
}
const resolvedOutputLocation = path.resolve(base, outputLocation);
await fs.writeFile(resolvedOutputLocation, contents);
};

await writeTada();
try {
await writeTada();
} catch (error) {
console.error('Something went wrong while writing the introspection file', error);
}
}

type SchemaOrigin = {
url: string;
headers: Record<string, unknown>;
};
export type SchemaOrigin =
| string
| {
url: string;
headers: Record<string, unknown>;
};

export const loadSchema = async (
root: string,
schema: SchemaOrigin | string
schema: SchemaOrigin
): Promise<GraphQLSchema | undefined> => {
let url: URL | undefined;
let config: { headers: Record<string, unknown> } | undefined;
Expand Down Expand Up @@ -161,40 +128,3 @@ export const loadSchema = async (
: schemaOrIntrospection;
}
};

const preambleComments = ['/* eslint-disable */', '/* prettier-ignore */'].join('\n') + '\n';

const dtsAnnotationComment = [
'/** An IntrospectionQuery representation of your schema.',
' *',
' * @remarks',
' * This is an introspection of your schema saved as a file by GraphQLSP.',
' * It will automatically be used by `gql.tada` to infer the types of your GraphQL documents.',
' * If you need to reuse this data or update your `scalars`, update `tadaOutputLocation` to',
' * instead save to a .ts instead of a .d.ts file.',
' */',
].join('\n');

const tsAnnotationComment = [
'/** An IntrospectionQuery representation of your schema.',
' *',
' * @remarks',
' * This is an introspection of your schema saved as a file by GraphQLSP.',
' * You may import it to create a `graphql()` tag function with `gql.tada`',
' * by importing it and passing it to `initGraphQLTada<>()`.',
' *',
' * @example',
' * ```',
" * import { initGraphQLTada } from 'gql.tada';",
" * import type { introspection } from './introspection';",
' *',
' * export const graphql = initGraphQLTada<{',
' * introspection: typeof introspection;',
' * scalars: {',
' * DateTime: string;',
' * Json: any;',
' * };',
' * }>();',
' * ```',
' */',
].join('\n');
8 changes: 4 additions & 4 deletions packages/internal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,16 @@
"prepublishOnly": "run-s clean build"
},
"devDependencies": {
"@urql/introspection": "^1.0.3",
"@types/node": "^20.11.0",
"json5": "^2.2.3",
"rollup": "^4.9.4",
"sade": "^1.8.1",
"type-fest": "^4.10.2",
"typescript": "^5.3.3"
"type-fest": "^4.10.2"
},
"dependencies": {
"@urql/introspection": "^1.0.3",
"graphql": "^16.8.1"
"graphql": "^16.8.1",
"typescript": "^5.3.3"
},
"publishConfig": {
"access": "public",
Expand Down
38 changes: 38 additions & 0 deletions packages/internal/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const PREAMBLE_IGNORE = ['/* eslint-disable */', '/* prettier-ignore */'].join('\n') + '\n';

const ANNOTATION_DTS = [
'/** An IntrospectionQuery representation of your schema.',
' *',
' * @remarks',
' * This is an introspection of your schema saved as a file by GraphQLSP.',
' * It will automatically be used by `gql.tada` to infer the types of your GraphQL documents.',
' * If you need to reuse this data or update your `scalars`, update `tadaOutputLocation` to',
' * instead save to a .ts instead of a .d.ts file.',
' */',
].join('\n');

const ANNOTATION_TS = [
'/** An IntrospectionQuery representation of your schema.',
' *',
' * @remarks',
' * This is an introspection of your schema saved as a file by GraphQLSP.',
' * You may import it to create a `graphql()` tag function with `gql.tada`',
' * by importing it and passing it to `initGraphQLTada<>()`.',
' *',
' * @example',
' * ```',
" * import { initGraphQLTada } from 'gql.tada';",
" * import type { introspection } from './introspection';",
' *',
' * export const graphql = initGraphQLTada<{',
' * introspection: typeof introspection;',
' * scalars: {',
' * DateTime: string;',
' * Json: any;',
' * };',
' * }>();',
' * ```',
' */',
].join('\n');

export { PREAMBLE_IGNORE, ANNOTATION_DTS, ANNOTATION_TS };
17 changes: 17 additions & 0 deletions packages/internal/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Diagnostic } from 'typescript';

export class TSError extends Error {
diagnostics: readonly Diagnostic[];
constructor(message: string, diagnostics?: readonly Diagnostic[]) {
super(message);
this.name = 'TSError';
this.diagnostics = diagnostics || [];
}
}

export class TadaError extends Error {
constructor(message: string) {
super(message);
this.name = 'TadaError';
}
}
8 changes: 8 additions & 0 deletions packages/internal/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
export * from './vfs';

export {
minifyIntrospection,
preprocessIntrospection,
outputIntrospectionFile,
} from './introspection';

export { resolveTypeScriptRootDir } from './resolve';
Loading
Loading