From 474a301922b33f09ccda397a715602bc0dfaa25c Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 20 Mar 2024 10:03:10 +0000 Subject: [PATCH 01/12] Add cleaned up implementation for TS virtual FS Host --- packages/cli-utils/src/vfs/compilerOptions.ts | 27 ++++ packages/cli-utils/src/vfs/host.ts | 67 ++++++++++ packages/cli-utils/src/vfs/index.ts | 116 ++++++++++++++++++ packages/cli-utils/src/vfs/utils.ts | 31 +++++ 4 files changed, 241 insertions(+) create mode 100644 packages/cli-utils/src/vfs/compilerOptions.ts create mode 100644 packages/cli-utils/src/vfs/host.ts create mode 100644 packages/cli-utils/src/vfs/index.ts create mode 100644 packages/cli-utils/src/vfs/utils.ts diff --git a/packages/cli-utils/src/vfs/compilerOptions.ts b/packages/cli-utils/src/vfs/compilerOptions.ts new file mode 100644 index 00000000..f4c52271 --- /dev/null +++ b/packages/cli-utils/src/vfs/compilerOptions.ts @@ -0,0 +1,27 @@ +import type { CompilerOptions } from 'typescript'; + +import { ScriptTarget, JsxEmit, ModuleResolutionKind } from 'typescript'; + +export const compilerOptions: CompilerOptions = { + rootDir: '/', + moduleResolution: ModuleResolutionKind.Bundler, + skipLibCheck: true, + skipDefaultLibCheck: true, + allowImportingTsExtensions: true, + allowSyntheticDefaultImports: true, + resolvePackageJsonExports: true, + resolvePackageJsonImports: true, + resolveJsonModule: true, + esModuleInterop: true, + jsx: JsxEmit.Preserve, + target: ScriptTarget.Latest, + checkJs: false, + allowJs: true, + strict: true, + noEmit: true, + noLib: false, + disableReferencedProjectLoad: true, + disableSourceOfProjectReferenceRedirect: true, + disableSizeLimit: true, + disableSolutionSearching: true, +}; diff --git a/packages/cli-utils/src/vfs/host.ts b/packages/cli-utils/src/vfs/host.ts new file mode 100644 index 00000000..cc65ad75 --- /dev/null +++ b/packages/cli-utils/src/vfs/host.ts @@ -0,0 +1,67 @@ +import type { ScriptTarget, CreateSourceFileOptions, SourceFile } from 'typescript'; + +import { createSourceFile } from 'typescript'; +import { posix as path } from 'node:path'; + +import type { TargetCache } from './utils'; +import { createTargetCache, setTargetCache, getTargetCache } from './utils'; + +export type FileData = Uint8Array | string; +export type Files = Record; + +export class File { + cache: TargetCache; + name: string; + data: Uint8Array | null; + text: string | null; + constructor(name: string, data: Uint8Array | string) { + this.cache = createTargetCache(); + this.name = name; + if (typeof data === 'string') { + this.text = data; + this.data = null; + } else { + this.text = null; + this.data = data; + } + } + + toSourceFile(opts: ScriptTarget | CreateSourceFileOptions) { + const target = typeof opts === 'object' ? opts.languageVersion : opts; + return ( + getTargetCache(this.cache, target) || + setTargetCache(this.cache, target, createSourceFile(this.name, this.toString(), opts)) + ); + } + + toBuffer(): Uint8Array { + return this.data || (this.data = new TextEncoder().encode(this.text!)); + } + + toString() { + return this.text || (this.text = new TextDecoder().decode(this.data!)); + } +} + +export class Directory { + children: Record; + files: Record; + constructor() { + this.children = Object.create(null); + this.files = Object.create(null); + } + + getOrCreateDirectory(name: string): Directory { + return this.children[name] || (this.children[name] = new Directory()); + } +} + +export function normalize(filename: string) { + return path.normalize(!filename.startsWith(path.sep) ? path.sep + filename : filename); +} + +export function split(filename: string): string[] { + return filename !== path.sep ? filename.split(path.sep).slice(1) : []; +} + +export const sep = path.sep; diff --git a/packages/cli-utils/src/vfs/index.ts b/packages/cli-utils/src/vfs/index.ts new file mode 100644 index 00000000..b51f64af --- /dev/null +++ b/packages/cli-utils/src/vfs/index.ts @@ -0,0 +1,116 @@ +import type { + CompilerHost, + ResolvedModule, + CreateSourceFileOptions, + ScriptTarget, +} from 'typescript'; + +import { createModuleResolutionCache, resolveModuleName } from 'typescript'; + +import { compilerOptions } from './compilerOptions'; +import { Directory, File, split, normalize, sep } from './host'; + +const ROOT_LIB_DTS_PATH = 'lib.d.ts'; +const ROOT_LIB_DTS_DATA = ''; + +export type VirtualCompilerHost = ReturnType & CompilerHost; + +export function createVirtualHost() { + const cache = createModuleResolutionCache(sep, normalize, compilerOptions); + + const root = new Directory(); + root.files[ROOT_LIB_DTS_PATH] = new File(ROOT_LIB_DTS_PATH, ROOT_LIB_DTS_DATA); + + return { + getCanonicalFileName: normalize, + + getDefaultLibFileName() { + return sep + ROOT_LIB_DTS_PATH; + }, + getCurrentDirectory() { + return sep; + }, + getNewLine() { + return '\n'; + }, + getModuleResolutionCache() { + return cache; + }, + useCaseSensitiveFileNames() { + return true; + }, + + fileExists(filename: string) { + const parts = split(normalize(filename)); + let directory: Directory | undefined = root; + for (let i = 0; i < parts.length - 1; i++) { + directory = directory.children[parts[i]]; + if (!directory) return false; + } + return !!directory.files[parts[parts.length - 1]]; + }, + + directoryExists(directoryName: string) { + const parts = split(normalize(directoryName)); + let directory: Directory | undefined = root; + for (let i = 0; i < parts.length - 1; i++) { + directory = directory.children[parts[i]]; + if (!directory) return false; + } + return !!directory.children[parts[parts.length - 1]]; + }, + + writeFile(filename: string, content: Uint8Array | string) { + const name = normalize(filename); + const parts = split(name); + let directory = root; + for (let i = 0; i < parts.length - 1; i++) + directory = directory.getOrCreateDirectory(parts[i]); + directory.files[parts[parts.length - 1]] = new File(name, content); + }, + + getDirectories(directoryName: string) { + const parts = split(normalize(directoryName)); + let directory: Directory | undefined = root; + for (let i = 0; i < parts.length; i++) { + directory = directory.children[parts[i]]; + if (!directory) return []; + } + return Object.keys(directory.children); + }, + + readFile(filename: string) { + const parts = split(normalize(filename)); + let directory: Directory | undefined = root; + for (let i = 0; i < parts.length - 1; i++) { + directory = directory.children[parts[i]]; + if (!directory) return undefined; + } + const file = directory.files[parts[parts.length - 1]]; + return file && file.toString(); + }, + + getSourceFile( + filename: string, + languageVersionOrOptions: ScriptTarget | CreateSourceFileOptions + ) { + const parts = split(normalize(filename)); + let directory: Directory | undefined = root; + for (let i = 0; i < parts.length - 1; i++) { + directory = directory.children[parts[i]]; + if (!directory) return undefined; + } + const file = directory.files[parts[parts.length - 1]]; + return file && file.toSourceFile(languageVersionOrOptions); + }, + + resolveModuleNames(moduleNames: string[], containingFile: string) { + const resolvedModules: (ResolvedModule | undefined)[] = []; + for (const moduleName of moduleNames) { + const result = resolveModuleName(moduleName, containingFile, compilerOptions, this, cache); + resolvedModules.push(result.resolvedModule); + } + return resolvedModules; + }, + } satisfies CompilerHost; +} diff --git a/packages/cli-utils/src/vfs/utils.ts b/packages/cli-utils/src/vfs/utils.ts new file mode 100644 index 00000000..b1b9b0a9 --- /dev/null +++ b/packages/cli-utils/src/vfs/utils.ts @@ -0,0 +1,31 @@ +import { ScriptTarget } from 'typescript'; + +export type TargetCache = { readonly _opaque: unique symbol } & Record; + +function createTargetCache(): TargetCache { + return { + [ScriptTarget[ScriptTarget.ES3]]: null, + [ScriptTarget[ScriptTarget.ES5]]: null, + [ScriptTarget[ScriptTarget.ES2015]]: null, + [ScriptTarget[ScriptTarget.ES2016]]: null, + [ScriptTarget[ScriptTarget.ES2017]]: null, + [ScriptTarget[ScriptTarget.ES2018]]: null, + [ScriptTarget[ScriptTarget.ES2019]]: null, + [ScriptTarget[ScriptTarget.ES2020]]: null, + [ScriptTarget[ScriptTarget.ES2021]]: null, + [ScriptTarget[ScriptTarget.ES2022]]: null, + [ScriptTarget[ScriptTarget.Latest]]: null, + [ScriptTarget[ScriptTarget.JSON]]: null, + } as TargetCache; +} + +function setTargetCache(cache: TargetCache, key: ScriptTarget, value: T): T { + cache[ScriptTarget[key]] = value; + return value; +} + +function getTargetCache(cache: TargetCache, key: ScriptTarget): T | null { + return cache[ScriptTarget[key]] || null; +} + +export { createTargetCache, setTargetCache, getTargetCache }; From eb7031b49b4757ebf85a9fb8d9b396b68e989cff Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 20 Mar 2024 11:22:51 +0000 Subject: [PATCH 02/12] Fix vjs quirks --- packages/cli-utils/src/vfs/host.ts | 4 ++-- packages/cli-utils/src/vfs/index.ts | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/cli-utils/src/vfs/host.ts b/packages/cli-utils/src/vfs/host.ts index cc65ad75..3cb941b6 100644 --- a/packages/cli-utils/src/vfs/host.ts +++ b/packages/cli-utils/src/vfs/host.ts @@ -16,9 +16,9 @@ export class File { text: string | null; constructor(name: string, data: Uint8Array | string) { this.cache = createTargetCache(); - this.name = name; + this.name = normalize(name); if (typeof data === 'string') { - this.text = data; + this.text = data || '\n'; this.data = null; } else { this.text = null; diff --git a/packages/cli-utils/src/vfs/index.ts b/packages/cli-utils/src/vfs/index.ts index b51f64af..1d7767ca 100644 --- a/packages/cli-utils/src/vfs/index.ts +++ b/packages/cli-utils/src/vfs/index.ts @@ -5,7 +5,11 @@ import type { ScriptTarget, } from 'typescript'; -import { createModuleResolutionCache, resolveModuleName } from 'typescript'; +import { + createProgram as tsCreateProgram, + createModuleResolutionCache, + resolveModuleName, +} from 'typescript'; import { compilerOptions } from './compilerOptions'; import { Directory, File, split, normalize, sep } from './host'; @@ -15,6 +19,9 @@ const ROOT_LIB_DTS_DATA = ''; export type VirtualCompilerHost = ReturnType & CompilerHost; +export const createProgram = (rootNames: string[], host: CompilerHost) => + tsCreateProgram(rootNames, compilerOptions, host); + export function createVirtualHost() { const cache = createModuleResolutionCache(sep, normalize, compilerOptions); From 4fbc2818060bacfb00d6077b60362efed7c885ea Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 20 Mar 2024 12:15:39 +0000 Subject: [PATCH 03/12] Add module loading --- packages/cli-utils/src/vfs/import.ts | 81 ++++++++++++++++++++++++++++ packages/cli-utils/src/vfs/index.ts | 3 ++ 2 files changed, 84 insertions(+) create mode 100644 packages/cli-utils/src/vfs/import.ts diff --git a/packages/cli-utils/src/vfs/import.ts b/packages/cli-utils/src/vfs/import.ts new file mode 100644 index 00000000..862abafe --- /dev/null +++ b/packages/cli-utils/src/vfs/import.ts @@ -0,0 +1,81 @@ +import type { CompilerHost } from 'typescript'; + +import path from 'node:path'; +import fs from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import { compilerOptions } from './compilerOptions'; + +export const requireResolve = + typeof require === 'function' ? require.resolve : createRequire(import.meta.url).resolve; + +const toPath = (input: string) => input.split(path.sep).join('/'); + +export async function importModule(host: CompilerHost, id: string) { + const request = `${id}/package.json`; + const module = requireResolve(request, { + paths: ['node_modules', ...(requireResolve.paths(request) || [])], + }); + if (!module) { + throw new Error(`Failed to resolve "${id}"`); + } + + const fromBasePath = path.dirname(module); + const toBasePath = `/node_modules/${id}/`; + + async function walk(directory: string) { + for (const entry of await fs.readdir(directory)) { + const fromFilePath = path.join(directory, entry); + if ((await fs.stat(fromFilePath)).isDirectory()) { + await walk(fromFilePath); + } else { + const toFilePath = path.join(path.relative(fromBasePath, directory), entry); + const data = await fs.readFile(fromFilePath, { encoding: 'utf8' }); + host.writeFile(toBasePath + toPath(toFilePath), data, false); + } + } + } + + await walk(fromBasePath); +} + +export async function importLib(host: CompilerHost) { + const request = 'typescript/package.json'; + const module = requireResolve(request, { + paths: ['node_modules', ...(requireResolve.paths(request) || [])], + }); + if (!module) { + throw new Error('Failed to resolve typescript'); + } + + const LIB_PATH = path.join(path.dirname(module), 'lib'); + const LIB_FILES = [ + 'lib.es5.d.ts', + 'lib.es2015.symbol.d.ts', + 'lib.es2015.collection.d.ts', + 'lib.es2015.iterable.d.ts', + ]; + + const contents = ( + await Promise.all( + LIB_FILES.map((libFile) => fs.readFile(path.resolve(LIB_PATH, libFile), { encoding: 'utf8' })) + ) + ).join('\n'); + + host.writeFile(host.getDefaultLibFileName(compilerOptions), contents, false); +} + +export async function resolveModuleFile(from: string) { + const slashIndex = from.indexOf('/'); + const id = from.slice(0, slashIndex); + const subpath = from.slice(slashIndex); + const request = `${id}/package.json`; + const module = requireResolve(request, { + paths: ['node_modules', ...(requireResolve.paths(request) || [])], + }); + if (!module) { + throw new Error(`Failed to resolve "${id}"`); + } + + const fromFilePath = path.join(path.dirname(module), subpath); + return fs.readFile(fromFilePath, { encoding: 'utf8' }); +} diff --git a/packages/cli-utils/src/vfs/index.ts b/packages/cli-utils/src/vfs/index.ts index 1d7767ca..d30e6cd3 100644 --- a/packages/cli-utils/src/vfs/index.ts +++ b/packages/cli-utils/src/vfs/index.ts @@ -17,6 +17,8 @@ import { Directory, File, split, normalize, sep } from './host'; const ROOT_LIB_DTS_PATH = 'lib.d.ts'; const ROOT_LIB_DTS_DATA = ''; +export { importLib, importModule, resolveModuleFile } from './import'; + export type VirtualCompilerHost = ReturnType & CompilerHost; export const createProgram = (rootNames: string[], host: CompilerHost) => @@ -59,6 +61,7 @@ export function createVirtualHost() { directoryExists(directoryName: string) { const parts = split(normalize(directoryName)); + if (!parts.length) return true; let directory: Directory | undefined = root; for (let i = 0; i < parts.length - 1; i++) { directory = directory.children[parts[i]]; From 6465a2293eb1edeabe6fcd499dbc3baa27b824e1 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 20 Mar 2024 13:11:05 +0000 Subject: [PATCH 04/12] Add initial introspection outputter --- packages/cli-utils/package.json | 8 +- packages/cli-utils/src/index.ts | 33 ++++++++- packages/cli-utils/src/introspection.ts | 98 +++++++++++++++++++++++++ packages/cli-utils/src/resolve.ts | 19 +++++ packages/cli-utils/src/tada.ts | 14 ++-- tsconfig.json | 2 +- 6 files changed, 162 insertions(+), 12 deletions(-) create mode 100644 packages/cli-utils/src/introspection.ts create mode 100644 packages/cli-utils/src/resolve.ts diff --git a/packages/cli-utils/package.json b/packages/cli-utils/package.json index c10a81ee..0dff8ce9 100644 --- a/packages/cli-utils/package.json +++ b/packages/cli-utils/package.json @@ -44,13 +44,13 @@ "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" + "@urql/introspection": "^1.0.3", + "graphql": "^16.8.1", + "typescript": "^5.3.3" }, "publishConfig": { "access": "public", diff --git a/packages/cli-utils/src/index.ts b/packages/cli-utils/src/index.ts index b9c28c63..178d785e 100644 --- a/packages/cli-utils/src/index.ts +++ b/packages/cli-utils/src/index.ts @@ -10,6 +10,7 @@ import { resolveTypeScriptRootDir } from '@gql.tada/internal'; import type { GraphQLSPConfig } from './lsp'; import { hasGraphQLSP } from './lsp'; import { ensureTadaIntrospection } from './tada'; +import { outputInternalTadaIntrospection } from './introspection'; interface GenerateSchemaOptions { headers?: Record; @@ -149,6 +150,34 @@ export async function generateTadaTypes(cwd: string = process.cwd()) { await ensureTadaIntrospection(foundPlugin.schema, foundPlugin.tadaOutputLocation!, cwd); } +export async function generateTadaIntrospection(cwd: string = process.cwd()) { + const tsconfigpath = path.resolve(cwd, 'tsconfig.json'); + const hasTsConfig = existsSync(tsconfigpath); + if (!hasTsConfig) { + console.error('Missing tsconfig.json'); + return; + } + + const tsconfigContents = await fs.readFile(tsconfigpath, 'utf-8'); + let tsConfig: TsConfigJson; + try { + tsConfig = parse(tsconfigContents) as TsConfigJson; + } catch (err) { + console.error(err); + return; + } + + if (!hasGraphQLSP(tsConfig)) { + return; + } + + const foundPlugin = tsConfig.compilerOptions!.plugins!.find( + (plugin) => plugin.name === '@0no-co/graphqlsp' + ) as GraphQLSPConfig; + + await outputInternalTadaIntrospection(foundPlugin.schema, cwd); +} + const prog = sade('gql.tada'); prog.version(process.env.npm_package_version || '0.0.0'); @@ -188,7 +217,9 @@ async function main() { .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(() => generateTadaTypes()) + .command('generate-introspection') + .action(() => generateTadaIntrospection()); prog.parse(process.argv); } diff --git a/packages/cli-utils/src/introspection.ts b/packages/cli-utils/src/introspection.ts new file mode 100644 index 00000000..612b84ea --- /dev/null +++ b/packages/cli-utils/src/introspection.ts @@ -0,0 +1,98 @@ +import type { SchemaOrigin } from './tada'; + +import { introspectionFromSchema } from 'graphql'; +import { minifyIntrospectionQuery } from '@urql/introspection'; + +import { EmitHint, NodeBuilderFlags, NewLineKind, createPrinter } from 'typescript'; + +import { + createVirtualHost, + importLib, + importModule, + resolveModuleFile, + createProgram, +} from './vfs'; +import { loadSchema } from './tada'; + +const builderFlags = + NodeBuilderFlags.NoTruncation | + NodeBuilderFlags.GenerateNamesForShadowedTypeParams | + NodeBuilderFlags.NoTypeReduction | + NodeBuilderFlags.AllowEmptyTuple | + NodeBuilderFlags.InObjectTypeLiteral | + NodeBuilderFlags.InTypeAlias | + NodeBuilderFlags.IgnoreErrors; + +const boilerplateFile = ` + import type { introspection } from './introspection.ts'; + import type { mapIntrospection } from './gql-tada.ts'; + type obj = T extends { [key: string | number]: any } ? { [K in keyof T]: T[K] } : never; + export type output = obj>; +`; + +export async function outputInternalTadaIntrospection( + schemaLocation: SchemaOrigin, + base: string = process.cwd() +) { + const schema = await loadSchema(base, schemaLocation); + if (!schema) { + console.error('Something went wrong while trying to load the schema.'); + return; + } + + const introspection = minifyIntrospectionQuery( + introspectionFromSchema(schema, { + descriptions: false, + }), + { + includeDirectives: false, + includeEnums: true, + includeInputs: true, + includeScalars: true, + } + ); + + const introspectionFile = `export type introspection = ${JSON.stringify( + introspection, + null, + 2 + )};`; + + const host = createVirtualHost(); + await importLib(host); + await importModule(host, '@0no-co/graphql.web'); + host.writeFile('gql-tada.ts', await resolveModuleFile('gql.tada/dist/gql-tada.d.ts')); + host.writeFile('introspection.ts', introspectionFile); + host.writeFile('index.ts', boilerplateFile); + + const program = createProgram(['index.ts'], host); + const checker = program.getTypeChecker(); + const diagnostics = program.getSemanticDiagnostics(); + if (diagnostics.length) { + throw new Error('TypeScript failed to evaluate introspection'); + } + + const root = program.getSourceFile('index.ts'); + const rootSymbol = root && checker.getSymbolAtLocation(root); + const outputSymbol = rootSymbol && checker.getExportsOfModule(rootSymbol)[0]; + const declaration = outputSymbol && outputSymbol.declarations && outputSymbol.declarations[0]; + const type = declaration && checker.getTypeAtLocation(declaration); + if (!type) { + throw new Error('Something went wrong while evaluating introspection type.'); + } + + const printer = createPrinter({ + newLine: NewLineKind.LineFeed, + removeComments: true, + omitTrailingSemicolon: true, + noEmitHelpers: true, + }); + + const typeNode = checker.typeToTypeNode(type, declaration, builderFlags); + if (!typeNode) { + throw new Error('Something went wrong while evaluating introspection type node.'); + } + + const _output = printer.printNode(EmitHint.Unspecified, typeNode, root); + // TODO +} diff --git a/packages/cli-utils/src/resolve.ts b/packages/cli-utils/src/resolve.ts new file mode 100644 index 00000000..bc40c6ac --- /dev/null +++ b/packages/cli-utils/src/resolve.ts @@ -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' }); +}; diff --git a/packages/cli-utils/src/tada.ts b/packages/cli-utils/src/tada.ts index a0700dee..80412620 100644 --- a/packages/cli-utils/src/tada.ts +++ b/packages/cli-utils/src/tada.ts @@ -30,7 +30,7 @@ export { readFragment as useFragment } from 'gql.tada'; * this function. */ export async function ensureTadaIntrospection( - schemaLocation: SchemaOrigin | string, + schemaLocation: SchemaOrigin, outputLocation: string, base: string = process.cwd() ) { @@ -88,14 +88,16 @@ export async function ensureTadaIntrospection( await writeTada(); } -type SchemaOrigin = { - url: string; - headers: Record; -}; +export type SchemaOrigin = + | string + | { + url: string; + headers: Record; + }; export const loadSchema = async ( root: string, - schema: SchemaOrigin | string + schema: SchemaOrigin ): Promise => { let url: URL | undefined; let config: { headers: Record } | undefined; diff --git a/tsconfig.json b/tsconfig.json index 9bd98001..2920f832 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,7 @@ "lib": ["dom", "esnext"], "jsx": "react-jsx", "declaration": false, - "module": "es2015", + "module": "es2022", "moduleResolution": "node", "resolveJsonModule": true, "target": "esnext", From b9a27d315036bc8a1558dbe5f629a2175d7c6b43 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 20 Mar 2024 14:22:45 +0000 Subject: [PATCH 05/12] Integrate preprocessing into CLI properly --- packages/cli-utils/src/index.ts | 51 ++++++++----------------- packages/cli-utils/src/introspection.ts | 38 ++---------------- packages/cli-utils/src/tada.ts | 8 +++- 3 files changed, 25 insertions(+), 72 deletions(-) diff --git a/packages/cli-utils/src/index.ts b/packages/cli-utils/src/index.ts index 178d785e..47fe2dfd 100644 --- a/packages/cli-utils/src/index.ts +++ b/packages/cli-utils/src/index.ts @@ -10,7 +10,6 @@ import { resolveTypeScriptRootDir } from '@gql.tada/internal'; import type { GraphQLSPConfig } from './lsp'; import { hasGraphQLSP } from './lsp'; import { ensureTadaIntrospection } from './tada'; -import { outputInternalTadaIntrospection } from './introspection'; interface GenerateSchemaOptions { headers?: Record; @@ -118,7 +117,7 @@ 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) { @@ -147,35 +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); -} - -export async function generateTadaIntrospection(cwd: string = process.cwd()) { - const tsconfigpath = path.resolve(cwd, 'tsconfig.json'); - const hasTsConfig = existsSync(tsconfigpath); - if (!hasTsConfig) { - console.error('Missing tsconfig.json'); - return; - } - - const tsconfigContents = await fs.readFile(tsconfigpath, 'utf-8'); - let tsConfig: TsConfigJson; - try { - tsConfig = parse(tsconfigContents) as TsConfigJson; - } catch (err) { - console.error(err); - return; - } - - if (!hasGraphQLSP(tsConfig)) { - return; - } - - const foundPlugin = tsConfig.compilerOptions!.plugins!.find( - (plugin) => plugin.name === '@0no-co/graphqlsp' - ) as GraphQLSPConfig; - - await outputInternalTadaIntrospection(foundPlugin.schema, cwd); + await ensureTadaIntrospection( + foundPlugin.schema, + foundPlugin.tadaOutputLocation!, + cwd, + shouldPreprocess + ); } const prog = sade('gql.tada'); @@ -208,18 +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()) - .command('generate-introspection') - .action(() => generateTadaIntrospection()); + .action((options) => { + const shouldPreprocess = !!options.preprocess && options.preprocess !== 'false'; + return generateTadaTypes(shouldPreprocess); + }); prog.parse(process.argv); } diff --git a/packages/cli-utils/src/introspection.ts b/packages/cli-utils/src/introspection.ts index 612b84ea..622e55e8 100644 --- a/packages/cli-utils/src/introspection.ts +++ b/packages/cli-utils/src/introspection.ts @@ -1,8 +1,3 @@ -import type { SchemaOrigin } from './tada'; - -import { introspectionFromSchema } from 'graphql'; -import { minifyIntrospectionQuery } from '@urql/introspection'; - import { EmitHint, NodeBuilderFlags, NewLineKind, createPrinter } from 'typescript'; import { @@ -12,7 +7,6 @@ import { resolveModuleFile, createProgram, } from './vfs'; -import { loadSchema } from './tada'; const builderFlags = NodeBuilderFlags.NoTruncation | @@ -30,33 +24,8 @@ const boilerplateFile = ` export type output = obj>; `; -export async function outputInternalTadaIntrospection( - schemaLocation: SchemaOrigin, - base: string = process.cwd() -) { - const schema = await loadSchema(base, schemaLocation); - if (!schema) { - console.error('Something went wrong while trying to load the schema.'); - return; - } - - const introspection = minifyIntrospectionQuery( - introspectionFromSchema(schema, { - descriptions: false, - }), - { - includeDirectives: false, - includeEnums: true, - includeInputs: true, - includeScalars: true, - } - ); - - const introspectionFile = `export type introspection = ${JSON.stringify( - introspection, - null, - 2 - )};`; +export async function introspectionToSchemaType(introspectionJson: string): Promise { + const introspectionFile = `export type introspection = ${introspectionJson};`; const host = createVirtualHost(); await importLib(host); @@ -93,6 +62,5 @@ export async function outputInternalTadaIntrospection( throw new Error('Something went wrong while evaluating introspection type node.'); } - const _output = printer.printNode(EmitHint.Unspecified, typeNode, root); - // TODO + return printer.printNode(EmitHint.Unspecified, typeNode, root); } diff --git a/packages/cli-utils/src/tada.ts b/packages/cli-utils/src/tada.ts index 80412620..2af78c0c 100644 --- a/packages/cli-utils/src/tada.ts +++ b/packages/cli-utils/src/tada.ts @@ -9,6 +9,8 @@ import { } from 'graphql'; import { minifyIntrospectionQuery } from '@urql/introspection'; +import { introspectionToSchemaType } from './introspection'; + export const tadaGqlContents = `import { initGraphQLTada } from 'gql.tada'; import type { introspection } from './introspection'; @@ -32,7 +34,8 @@ export { readFragment as useFragment } from 'gql.tada'; export async function ensureTadaIntrospection( schemaLocation: SchemaOrigin, outputLocation: string, - base: string = process.cwd() + base: string = process.cwd(), + shouldPreprocess = false ) { const writeTada = async () => { try { @@ -56,10 +59,11 @@ export async function ensureTadaIntrospection( let contents; if (/\.d\.ts$/.test(outputLocation)) { + const outputType = shouldPreprocess ? await introspectionToSchemaType(json) : json; contents = [ preambleComments, dtsAnnotationComment, - `export type introspection = ${json};\n`, + `export type introspection = ${outputType};\n`, "import * as gqlTada from 'gql.tada';\n", "declare module 'gql.tada' {", ' interface setupSchema {', From 61cd189c0926d7d328daf23abe4dfdaf4404d6dd Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 20 Mar 2024 16:14:21 +0000 Subject: [PATCH 06/12] Update pnpm-lock.yaml --- pnpm-lock.yaml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 33c43073..cead17be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -158,6 +158,9 @@ importers: graphql: specifier: ^16.8.1 version: 16.8.1 + typescript: + specifier: ^5.3.3 + version: 5.3.3 devDependencies: '@types/node': specifier: ^20.11.0 @@ -174,9 +177,6 @@ importers: type-fest: specifier: ^4.10.2 version: 4.10.2 - typescript: - specifier: ^5.3.3 - version: 5.3.3 packages/internal: dependencies: @@ -204,7 +204,7 @@ importers: version: 4.10.2 typescript: specifier: ^5.3.3 - version: 5.3.3 + version: 5.4.2 website: dependencies: @@ -5987,7 +5987,6 @@ packages: resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} engines: {node: '>=14.17'} hasBin: true - dev: true /typescript@5.4.2: resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==} From 24e0b5a566869c3f93d85e4b090cf95c7b49397c Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 20 Mar 2024 16:21:38 +0000 Subject: [PATCH 07/12] Move vfs to internal package --- packages/internal/src/index.ts | 1 + packages/{cli-utils => internal}/src/vfs/compilerOptions.ts | 0 packages/{cli-utils => internal}/src/vfs/host.ts | 0 packages/{cli-utils => internal}/src/vfs/import.ts | 5 ++++- packages/{cli-utils => internal}/src/vfs/index.ts | 3 +++ packages/{cli-utils => internal}/src/vfs/utils.ts | 0 6 files changed, 8 insertions(+), 1 deletion(-) rename packages/{cli-utils => internal}/src/vfs/compilerOptions.ts (100%) rename packages/{cli-utils => internal}/src/vfs/host.ts (100%) rename packages/{cli-utils => internal}/src/vfs/import.ts (97%) rename packages/{cli-utils => internal}/src/vfs/index.ts (98%) rename packages/{cli-utils => internal}/src/vfs/utils.ts (100%) diff --git a/packages/internal/src/index.ts b/packages/internal/src/index.ts index 649dc2e4..f5add1fc 100644 --- a/packages/internal/src/index.ts +++ b/packages/internal/src/index.ts @@ -1 +1,2 @@ +export * from './vfs'; export { resolveTypeScriptRootDir } from './resolve'; diff --git a/packages/cli-utils/src/vfs/compilerOptions.ts b/packages/internal/src/vfs/compilerOptions.ts similarity index 100% rename from packages/cli-utils/src/vfs/compilerOptions.ts rename to packages/internal/src/vfs/compilerOptions.ts diff --git a/packages/cli-utils/src/vfs/host.ts b/packages/internal/src/vfs/host.ts similarity index 100% rename from packages/cli-utils/src/vfs/host.ts rename to packages/internal/src/vfs/host.ts diff --git a/packages/cli-utils/src/vfs/import.ts b/packages/internal/src/vfs/import.ts similarity index 97% rename from packages/cli-utils/src/vfs/import.ts rename to packages/internal/src/vfs/import.ts index 862abafe..3bf07912 100644 --- a/packages/cli-utils/src/vfs/import.ts +++ b/packages/internal/src/vfs/import.ts @@ -5,11 +5,12 @@ import fs from 'node:fs/promises'; import { createRequire } from 'node:module'; import { compilerOptions } from './compilerOptions'; -export const requireResolve = +const requireResolve = typeof require === 'function' ? require.resolve : createRequire(import.meta.url).resolve; const toPath = (input: string) => input.split(path.sep).join('/'); +/** @internal */ export async function importModule(host: CompilerHost, id: string) { const request = `${id}/package.json`; const module = requireResolve(request, { @@ -38,6 +39,7 @@ export async function importModule(host: CompilerHost, id: string) { await walk(fromBasePath); } +/** @internal */ export async function importLib(host: CompilerHost) { const request = 'typescript/package.json'; const module = requireResolve(request, { @@ -64,6 +66,7 @@ export async function importLib(host: CompilerHost) { host.writeFile(host.getDefaultLibFileName(compilerOptions), contents, false); } +/** @internal */ export async function resolveModuleFile(from: string) { const slashIndex = from.indexOf('/'); const id = from.slice(0, slashIndex); diff --git a/packages/cli-utils/src/vfs/index.ts b/packages/internal/src/vfs/index.ts similarity index 98% rename from packages/cli-utils/src/vfs/index.ts rename to packages/internal/src/vfs/index.ts index d30e6cd3..34f5f5ea 100644 --- a/packages/cli-utils/src/vfs/index.ts +++ b/packages/internal/src/vfs/index.ts @@ -19,11 +19,14 @@ const ROOT_LIB_DTS_DATA = ''; export { importLib, importModule, resolveModuleFile } from './import'; +/** @internal */ export type VirtualCompilerHost = ReturnType & CompilerHost; +/** @internal */ export const createProgram = (rootNames: string[], host: CompilerHost) => tsCreateProgram(rootNames, compilerOptions, host); +/** @internal */ export function createVirtualHost() { const cache = createModuleResolutionCache(sep, normalize, compilerOptions); diff --git a/packages/cli-utils/src/vfs/utils.ts b/packages/internal/src/vfs/utils.ts similarity index 100% rename from packages/cli-utils/src/vfs/utils.ts rename to packages/internal/src/vfs/utils.ts From f82a70936ac239aba9c3d4a72f79cc54b66d1203 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 20 Mar 2024 16:46:43 +0000 Subject: [PATCH 08/12] Add introspection formats to @gql.tada/internal --- packages/internal/package.json | 2 +- packages/internal/src/constants.ts | 38 ++++++++ packages/internal/src/errors.ts | 17 ++++ packages/internal/src/index.ts | 7 ++ packages/internal/src/introspection.ts | 124 +++++++++++++++++++++++++ 5 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 packages/internal/src/constants.ts create mode 100644 packages/internal/src/errors.ts create mode 100644 packages/internal/src/introspection.ts diff --git a/packages/internal/package.json b/packages/internal/package.json index a888cd66..9ae5769b 100644 --- a/packages/internal/package.json +++ b/packages/internal/package.json @@ -40,6 +40,7 @@ "prepublishOnly": "run-s clean build" }, "devDependencies": { + "@urql/introspection": "^1.0.3", "@types/node": "^20.11.0", "json5": "^2.2.3", "rollup": "^4.9.4", @@ -48,7 +49,6 @@ "typescript": "^5.3.3" }, "dependencies": { - "@urql/introspection": "^1.0.3", "graphql": "^16.8.1" }, "publishConfig": { diff --git a/packages/internal/src/constants.ts b/packages/internal/src/constants.ts new file mode 100644 index 00000000..b3715e6a --- /dev/null +++ b/packages/internal/src/constants.ts @@ -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 }; diff --git a/packages/internal/src/errors.ts b/packages/internal/src/errors.ts new file mode 100644 index 00000000..c7e83218 --- /dev/null +++ b/packages/internal/src/errors.ts @@ -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'; + } +} diff --git a/packages/internal/src/index.ts b/packages/internal/src/index.ts index f5add1fc..7941f677 100644 --- a/packages/internal/src/index.ts +++ b/packages/internal/src/index.ts @@ -1,2 +1,9 @@ export * from './vfs'; + +export { + minifyIntrospection, + preprocessIntrospection, + outputIntrospectionFile, +} from './introspection'; + export { resolveTypeScriptRootDir } from './resolve'; diff --git a/packages/internal/src/introspection.ts b/packages/internal/src/introspection.ts new file mode 100644 index 00000000..e9f081e8 --- /dev/null +++ b/packages/internal/src/introspection.ts @@ -0,0 +1,124 @@ +import type { IntrospectionQuery } from 'graphql'; +import { EmitHint, NodeBuilderFlags, NewLineKind, createPrinter } from 'typescript'; +import { minifyIntrospectionQuery } from '@urql/introspection'; + +import { PREAMBLE_IGNORE, ANNOTATION_DTS, ANNOTATION_TS } from './constants'; +import { TSError, TadaError } from './errors'; + +import { + createVirtualHost, + importLib, + importModule, + resolveModuleFile, + createProgram, +} from './vfs'; + +const builderFlags = + NodeBuilderFlags.NoTruncation | + NodeBuilderFlags.GenerateNamesForShadowedTypeParams | + NodeBuilderFlags.NoTypeReduction | + NodeBuilderFlags.AllowEmptyTuple | + NodeBuilderFlags.InObjectTypeLiteral | + NodeBuilderFlags.InTypeAlias | + NodeBuilderFlags.IgnoreErrors; + +const boilerplateFile = ` + import type { introspection } from './introspection.ts'; + import type { mapIntrospection } from './gql-tada.ts'; + type obj = T extends { [key: string | number]: any } ? { [K in keyof T]: T[K] } : never; + export type output = obj>; +`; + +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, + }); +} + +export async function preprocessIntrospection(introspection: IntrospectionQuery): Promise { + const json = JSON.stringify(introspection, null, 2); + const introspectionFile = `export type introspection = ${json};`; + + const host = createVirtualHost(); + await importLib(host); + await importModule(host, '@0no-co/graphql.web'); + host.writeFile('gql-tada.ts', await resolveModuleFile('gql.tada/dist/gql-tada.d.ts')); + host.writeFile('introspection.ts', introspectionFile); + host.writeFile('index.ts', boilerplateFile); + + const program = createProgram(['index.ts'], host); + const checker = program.getTypeChecker(); + const diagnostics = program.getSemanticDiagnostics(); + if (diagnostics.length) { + throw new TSError('TypeScript failed to evaluate introspection', diagnostics); + } + + const root = program.getSourceFile('index.ts'); + const rootSymbol = root && checker.getSymbolAtLocation(root); + const outputSymbol = rootSymbol && checker.getExportsOfModule(rootSymbol)[0]; + const declaration = outputSymbol && outputSymbol.declarations && outputSymbol.declarations[0]; + const type = declaration && checker.getTypeAtLocation(declaration); + if (!type) { + throw new TSError('Something went wrong while evaluating introspection type.'); + } + + const printer = createPrinter({ + newLine: NewLineKind.LineFeed, + removeComments: true, + omitTrailingSemicolon: true, + noEmitHelpers: true, + }); + + const typeNode = checker.typeToTypeNode(type, declaration, builderFlags); + if (!typeNode) { + throw new TSError('Something went wrong while evaluating introspection type node.'); + } + + return printer.printNode(EmitHint.Unspecified, typeNode, root); +} + +interface OutputIntrospectionFileOptions { + fileType: '.ts' | '.d.ts' | string; + shouldPreprocess?: boolean; +} + +export async function outputIntrospectionFile( + introspection: IntrospectionQuery | string, + opts: OutputIntrospectionFileOptions +): Promise { + if (/\.d\.ts$/.test(opts.fileType)) { + const json = + typeof introspection !== 'string' && opts.shouldPreprocess + ? await 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")` + ); +} From ed047ab4d4f427c5ac1765fb5e6b738d6ab7766d Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 20 Mar 2024 16:53:57 +0000 Subject: [PATCH 09/12] Use @gql.tada/internal tools in CLI --- packages/cli-utils/package.json | 4 +- packages/cli-utils/src/introspection.ts | 66 ------------- packages/cli-utils/src/tada.ts | 124 +++++------------------- packages/internal/package.json | 6 +- pnpm-lock.yaml | 15 +-- 5 files changed, 33 insertions(+), 182 deletions(-) delete mode 100644 packages/cli-utils/src/introspection.ts diff --git a/packages/cli-utils/package.json b/packages/cli-utils/package.json index 0dff8ce9..2f3230e3 100644 --- a/packages/cli-utils/package.json +++ b/packages/cli-utils/package.json @@ -48,9 +48,7 @@ }, "dependencies": { "@gql.tada/internal": "workspace:*", - "@urql/introspection": "^1.0.3", - "graphql": "^16.8.1", - "typescript": "^5.3.3" + "graphql": "^16.8.1" }, "publishConfig": { "access": "public", diff --git a/packages/cli-utils/src/introspection.ts b/packages/cli-utils/src/introspection.ts deleted file mode 100644 index 622e55e8..00000000 --- a/packages/cli-utils/src/introspection.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { EmitHint, NodeBuilderFlags, NewLineKind, createPrinter } from 'typescript'; - -import { - createVirtualHost, - importLib, - importModule, - resolveModuleFile, - createProgram, -} from './vfs'; - -const builderFlags = - NodeBuilderFlags.NoTruncation | - NodeBuilderFlags.GenerateNamesForShadowedTypeParams | - NodeBuilderFlags.NoTypeReduction | - NodeBuilderFlags.AllowEmptyTuple | - NodeBuilderFlags.InObjectTypeLiteral | - NodeBuilderFlags.InTypeAlias | - NodeBuilderFlags.IgnoreErrors; - -const boilerplateFile = ` - import type { introspection } from './introspection.ts'; - import type { mapIntrospection } from './gql-tada.ts'; - type obj = T extends { [key: string | number]: any } ? { [K in keyof T]: T[K] } : never; - export type output = obj>; -`; - -export async function introspectionToSchemaType(introspectionJson: string): Promise { - const introspectionFile = `export type introspection = ${introspectionJson};`; - - const host = createVirtualHost(); - await importLib(host); - await importModule(host, '@0no-co/graphql.web'); - host.writeFile('gql-tada.ts', await resolveModuleFile('gql.tada/dist/gql-tada.d.ts')); - host.writeFile('introspection.ts', introspectionFile); - host.writeFile('index.ts', boilerplateFile); - - const program = createProgram(['index.ts'], host); - const checker = program.getTypeChecker(); - const diagnostics = program.getSemanticDiagnostics(); - if (diagnostics.length) { - throw new Error('TypeScript failed to evaluate introspection'); - } - - const root = program.getSourceFile('index.ts'); - const rootSymbol = root && checker.getSymbolAtLocation(root); - const outputSymbol = rootSymbol && checker.getExportsOfModule(rootSymbol)[0]; - const declaration = outputSymbol && outputSymbol.declarations && outputSymbol.declarations[0]; - const type = declaration && checker.getTypeAtLocation(declaration); - if (!type) { - throw new Error('Something went wrong while evaluating introspection type.'); - } - - const printer = createPrinter({ - newLine: NewLineKind.LineFeed, - removeComments: true, - omitTrailingSemicolon: true, - noEmitHelpers: true, - }); - - const typeNode = checker.typeToTypeNode(type, declaration, builderFlags); - if (!typeNode) { - throw new Error('Something went wrong while evaluating introspection type node.'); - } - - return printer.printNode(EmitHint.Unspecified, typeNode, root); -} diff --git a/packages/cli-utils/src/tada.ts b/packages/cli-utils/src/tada.ts index 2af78c0c..6b6bff92 100644 --- a/packages/cli-utils/src/tada.ts +++ b/packages/cli-utils/src/tada.ts @@ -1,28 +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'; - -import { introspectionToSchemaType } from './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 @@ -35,61 +22,35 @@ export async function ensureTadaIntrospection( schemaLocation: SchemaOrigin, outputLocation: string, base: string = process.cwd(), - shouldPreprocess = false + 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)) { - const outputType = shouldPreprocess ? await introspectionToSchemaType(json) : json; - contents = [ - preambleComments, - dtsAnnotationComment, - `export type introspection = ${outputType};\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); + } } export type SchemaOrigin = @@ -167,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'); diff --git a/packages/internal/package.json b/packages/internal/package.json index 9ae5769b..02ea29f4 100644 --- a/packages/internal/package.json +++ b/packages/internal/package.json @@ -45,11 +45,11 @@ "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": { - "graphql": "^16.8.1" + "graphql": "^16.8.1", + "typescript": "^5.3.3" }, "publishConfig": { "access": "public", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cead17be..1d2d8c3f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -152,15 +152,9 @@ importers: '@gql.tada/internal': specifier: workspace:* version: link:../internal - '@urql/introspection': - specifier: ^1.0.3 - version: 1.0.3(graphql@16.8.1) graphql: specifier: ^16.8.1 version: 16.8.1 - typescript: - specifier: ^5.3.3 - version: 5.3.3 devDependencies: '@types/node': specifier: ^20.11.0 @@ -180,9 +174,6 @@ importers: packages/internal: dependencies: - '@urql/introspection': - specifier: ^1.0.3 - version: 1.0.3(graphql@16.8.1) graphql: specifier: ^16.8.1 version: 16.8.1 @@ -190,6 +181,9 @@ importers: '@types/node': specifier: ^20.11.0 version: 20.11.0 + '@urql/introspection': + specifier: ^1.0.3 + version: 1.0.3(graphql@16.8.1) json5: specifier: ^2.2.3 version: 2.2.3 @@ -2143,7 +2137,7 @@ packages: graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 dependencies: graphql: 16.8.1 - dev: false + dev: true /@vitejs/plugin-react@4.2.1(vite@5.1.6): resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==} @@ -5987,6 +5981,7 @@ packages: resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} engines: {node: '>=14.17'} hasBin: true + dev: true /typescript@5.4.2: resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==} From 81a9f83cde62555c57017d2a33c5c0c7f4185950 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 20 Mar 2024 17:01:05 +0000 Subject: [PATCH 10/12] Add changeset --- .changeset/three-spoons-call.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/three-spoons-call.md diff --git a/.changeset/three-spoons-call.md b/.changeset/three-spoons-call.md new file mode 100644 index 00000000..d39ba46b --- /dev/null +++ b/.changeset/three-spoons-call.md @@ -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. From d9a8da2350674d9e0d5920a67b2301ac51c20bbf Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 20 Mar 2024 17:06:26 +0000 Subject: [PATCH 11/12] Update pnpm-lock.yaml --- pnpm-lock.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d2d8c3f..d5cae2e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -177,6 +177,9 @@ importers: graphql: specifier: ^16.8.1 version: 16.8.1 + typescript: + specifier: ^5.3.3 + version: 5.4.2 devDependencies: '@types/node': specifier: ^20.11.0 @@ -196,9 +199,6 @@ importers: type-fest: specifier: ^4.10.2 version: 4.10.2 - typescript: - specifier: ^5.3.3 - version: 5.4.2 website: dependencies: From 48aeae24f67cfdace6badb2e55dd30eb52b06690 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 20 Mar 2024 20:06:27 +0000 Subject: [PATCH 12/12] Fix tsconfig resolution in CLI --- packages/cli-utils/src/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/cli-utils/src/index.ts b/packages/cli-utils/src/index.ts index 47fe2dfd..0ce6170a 100644 --- a/packages/cli-utils/src/index.ts +++ b/packages/cli-utils/src/index.ts @@ -125,11 +125,11 @@ export async function generateTadaTypes(shouldPreprocess = false, cwd: string = 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;