diff --git a/.changeset/blue-boats-dance.md b/.changeset/blue-boats-dance.md new file mode 100644 index 00000000..8c905dc0 --- /dev/null +++ b/.changeset/blue-boats-dance.md @@ -0,0 +1,5 @@ +--- +'gql.tada': minor +--- + +Add CLI entrypoint `gql-tada` capable of generating the types file without the LSP running diff --git a/.changeset/nice-worms-deny.md b/.changeset/nice-worms-deny.md new file mode 100644 index 00000000..fda022bf --- /dev/null +++ b/.changeset/nice-worms-deny.md @@ -0,0 +1,5 @@ +--- +'@gql.tada/cli': minor +--- + +Add `tada` CLI capable of generating the introspection types file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8556520..96a5b714 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,10 +51,10 @@ jobs: run: pnpm run lint - name: Unit Tests - run: pnpm run test --run + run: pnpm multi --include-workspace-root run test --run - name: benchmarks - run: pnpm run bench --run + run: pnpm multi --include-workspace-root run bench --run - name: Build - run: pnpm run build + run: pnpm multi --include-workspace-root run build diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml index 7648efb9..c5e79189 100644 --- a/.github/workflows/site.yml +++ b/.github/workflows/site.yml @@ -49,7 +49,7 @@ jobs: - name: Build Website working-directory: website - run: pnpm run build + run: pnpm run export - name: Deploy to Cloudflare Pages uses: cloudflare/pages-action@v1 diff --git a/cli/package.json b/cli/package.json new file mode 100644 index 00000000..ff0c8707 --- /dev/null +++ b/cli/package.json @@ -0,0 +1,17 @@ +{ + "name": "gql-tada-cli", + "private": true, + "version": "0.0.0", + "main": "../dist/gql-tada-cli.js", + "module": "../dist/gql-tada-cli.mjs", + "types": "../dist/gql-tada-cli.d.ts", + "source": "../src/cli/index.ts", + "exports": { + ".": { + "types": "../dist/gql-tada-cli.d.ts", + "import": "../dist/gql-tada-cli.mjs", + "require": "../dist/gql-tada-cli.js", + "source": "../src/cli/index.ts" + } + } +} diff --git a/package.json b/package.json index bd1eea19..2bf2b313 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,10 @@ "module": "./dist/gql-tada.mjs", "types": "./dist/gql-tada.d.ts", "sideEffects": false, + "bin": { + "gql.tada": "./dist/gql-tada-cli.js", + "gql-tada": "./dist/gql-tada-cli.js" + }, "files": [ "LICENSE.md", "README.md", @@ -20,10 +24,17 @@ "require": "./dist/gql-tada.js", "source": "./src/index.ts" }, + "./cli": { + "types": "./dist/gql-tada-cli.d.ts", + "import": "./dist/gql-tada-cli.mjs", + "require": "./dist/gql-tada-cli.js", + "source": "./src/cli/index.ts" + }, "./package.json": "./package.json" }, "dependencies": { - "@0no-co/graphql.web": "^1.0.4" + "@0no-co/graphql.web": "^1.0.4", + "@gql.tada/cli-utils": "workspace:*" }, "public": true, "keywords": [ diff --git a/packages/cli-utils/LICENSE.md b/packages/cli-utils/LICENSE.md new file mode 100644 index 00000000..25ce8b2c --- /dev/null +++ b/packages/cli-utils/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 0no.co + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/cli-utils/README.md b/packages/cli-utils/README.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/cli-utils/package.json b/packages/cli-utils/package.json new file mode 100644 index 00000000..b8ebb377 --- /dev/null +++ b/packages/cli-utils/package.json @@ -0,0 +1,42 @@ +{ + "name": "@gql.tada/cli-utils", + "version": "0.0.0", + "description": "Main logic for gql.tada’s CLI tool.", + "author": "0no.co ", + "sideEffects": false, + "source": "./src/index.ts", + "exports": { + ".": { + "types": "./dist/gql-tada-cli.d.ts", + "import": "./dist/gql-tada-cli.mjs", + "require": "./dist/gql-tada-cli.js", + "source": "./src/index.ts" + }, + "./package.json": "./package.json" + }, + "files": [ + "CHANGELOG.md", + "LICENSE.md", + "README.md", + "dist/" + ], + "license": "MIT", + "scripts": { + "build": "rollup -c ../../scripts/rollup.config.mjs" + }, + "devDependencies": { + "@types/node": "^20.11.0", + "comment-json": "^4.2.3", + "rollup": "^4.9.4", + "sade": "^1.8.1", + "type-fest": "^4.10.2", + "typescript": "^5.3.3" + }, + "dependencies": { + "@urql/introspection": "^1.0.3" + }, + "publishConfig": { + "access": "public", + "provenance": true + } +} diff --git a/packages/cli-utils/src/index.ts b/packages/cli-utils/src/index.ts new file mode 100644 index 00000000..f1203909 --- /dev/null +++ b/packages/cli-utils/src/index.ts @@ -0,0 +1,88 @@ +import sade from 'sade'; +import { promises as fs, existsSync } from 'node:fs'; +import path from 'node:path'; +// We use comment-json to parse the tsconfig as the default ones +// have comment annotations in JSON. +import { parse } from 'comment-json'; +import type { TsConfigJson } from 'type-fest'; +import { ensureTadaIntrospection } from './tada'; + +const prog = sade('fuse'); + +prog.version(process.env.npm_package_version || '0.0.0'); + +type GraphQLSPConfig = { + name: string; + schema: string; + tadaOutputLocation?: string; +}; + +function hasGraphQLSP(tsconfig: TsConfigJson): boolean { + if (!tsconfig.compilerOptions) { + // Warn + return false; + } + + if (!tsconfig.compilerOptions.plugins) { + // Warn + return false; + } + + const foundPlugin = tsconfig.compilerOptions.plugins.find( + (plugin) => plugin.name === '@0no-co/graphqlsp' + ) as GraphQLSPConfig | undefined; + if (!foundPlugin) { + // Warn + return false; + } + + if (!foundPlugin.schema) { + // Warn + return false; + } + + if (!foundPlugin.tadaOutputLocation) { + // Warn + return false; + } + + return true; +} + +async function main() { + prog + .command('generate') + .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(async () => { + const cwd = process.cwd(); + const tsconfigpath = path.resolve(cwd, 'tsconfig.json'); + const hasTsConfig = existsSync(tsconfigpath); + if (!hasTsConfig) { + // Error + } + + const tsconfigContents = await fs.readFile(tsconfigpath, 'utf-8'); + let tsConfig: TsConfigJson; + try { + tsConfig = parse(tsconfigContents) as TsConfigJson; + } catch (err) { + // report error and bail + return; + } + + if (!hasGraphQLSP(tsConfig)) { + // Error + } + + const foundPlugin = tsConfig.compilerOptions!.plugins!.find( + (plugin) => plugin.name === '@0no-co/graphqlsp' + ) as GraphQLSPConfig; + + await ensureTadaIntrospection(foundPlugin.schema, false); + // Generate the file + }); +} + +export default main; diff --git a/packages/cli-utils/src/tada.ts b/packages/cli-utils/src/tada.ts new file mode 100644 index 00000000..0ad63755 --- /dev/null +++ b/packages/cli-utils/src/tada.ts @@ -0,0 +1,92 @@ +import { promises as fs, watch, existsSync } from 'node:fs'; +import path from 'node:path'; +import { buildSchema, 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'; +`; + +/** + * This function mimics the behavior of the LSP, this so we can ensure + * that gql.tada will work in any environment. The JetBrains IDE's do not + * implement the tsserver plugin protocol hence in those and editors where + * we are not able to leverage the workspace TS version we will rely on + * this function. + */ +export async function ensureTadaIntrospection(location: string, shouldWatch: boolean) { + const schemaLocation = path.resolve(location, 'schema.graphql'); + + const writeTada = async () => { + try { + const content = await fs.readFile(schemaLocation, 'utf-8'); + const schema = buildSchema(content); + const introspection = introspectionFromSchema(schema, { + descriptions: false, + }); + const minified = minifyIntrospectionQuery(introspection, { + includeDirectives: false, + includeEnums: true, + includeInputs: true, + includeScalars: true, + }); + + const json = JSON.stringify(minified, null, 2); + const hasSrcDir = existsSync(path.resolve(location, 'src')); + const base = hasSrcDir ? path.resolve(location, 'src') : location; + + const outputLocation = path.resolve(base, 'fuse', 'introspection.ts'); + const contents = [ + preambleComments, + tsAnnotationComment, + `const introspection = ${json} as const;\n`, + 'export { introspection };', + ].join('\n'); + + await fs.writeFile(outputLocation, contents); + } catch (e) {} + }; + + await writeTada(); + + if (shouldWatch) { + watch(schemaLocation, async () => { + await writeTada(); + }); + } +} + +const preambleComments = ['/* eslint-disable */', '/* prettier-ignore */'].join('\n') + '\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/cli-utils/tsconfig.json b/packages/cli-utils/tsconfig.json new file mode 100644 index 00000000..596e2cf7 --- /dev/null +++ b/packages/cli-utils/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fd12a3ad..1258541e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,9 @@ importers: '@0no-co/graphql.web': specifier: ^1.0.4 version: 1.0.4(graphql@16.8.1) + '@gql.tada/cli-utils': + specifier: workspace:* + version: link:packages/cli-utils devDependencies: '@0no-co/typescript.js': specifier: 5.3.2-2 @@ -141,6 +144,31 @@ importers: specifier: ^5.0.11 version: 5.0.11(@types/node@20.11.0)(terser@5.26.0) + packages/cli-utils: + dependencies: + '@urql/introspection': + specifier: ^1.0.3 + version: 1.0.3(graphql@16.8.1) + devDependencies: + '@types/node': + specifier: ^20.11.0 + version: 20.11.0 + comment-json: + specifier: ^4.2.3 + version: 4.2.3 + rollup: + specifier: ^4.9.4 + version: 4.9.4 + sade: + specifier: ^1.8.1 + version: 1.8.1 + type-fest: + specifier: ^4.10.2 + version: 4.10.2 + typescript: + specifier: ^5.3.3 + version: 5.3.3 + website: dependencies: '@astrojs/cloudflare': @@ -1963,6 +1991,14 @@ packages: - graphql dev: false + /@urql/introspection@1.0.3(graphql@16.8.1): + resolution: {integrity: sha512-5zgnfUDV10c3qudqYvfZ/rOtWVB2QvqanmoDMttqpt+TCCPkSUZdb2qcLCEB6DL7ph8mQRTZhXI29J57nTnqKg==} + peerDependencies: + 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 + /@vitejs/plugin-react@4.2.1(vite@5.0.11): resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==} engines: {node: ^14.18.0 || >=16.0.0} @@ -2200,6 +2236,10 @@ packages: resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==} dev: false + /array-timsort@1.0.3: + resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + dev: true + /array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -2754,6 +2794,17 @@ packages: engines: {node: '>= 6'} dev: false + /comment-json@4.2.3: + resolution: {integrity: sha512-SsxdiOf064DWoZLH799Ata6u7iV658A11PlWtZATDlXPpKGJnbJZ5Z24ybixAi+LUUqJ/GKowAejtC5GFUG7Tw==} + engines: {node: '>= 6'} + dependencies: + array-timsort: 1.0.3 + core-util-is: 1.0.3 + esprima: 4.0.1 + has-own-prop: 2.0.0 + repeat-string: 1.6.1 + dev: true + /common-ancestor-path@1.0.1: resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} dev: false @@ -2779,6 +2830,10 @@ packages: engines: {node: '>= 0.6'} dev: false + /core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + dev: true + /cross-spawn@5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} dependencies: @@ -3797,6 +3852,11 @@ packages: engines: {node: '>=8'} dev: true + /has-own-prop@2.0.0: + resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==} + engines: {node: '>=8'} + dev: true + /has-property-descriptors@1.0.1: resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} dependencies: @@ -5453,6 +5513,11 @@ packages: ufo: 1.3.2 dev: true + /mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + dev: true + /ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} dev: false @@ -6358,6 +6423,11 @@ packages: unified: 11.0.4 dev: false + /repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + dev: true + /request-light@0.7.0: resolution: {integrity: sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q==} dev: true @@ -6513,6 +6583,13 @@ packages: dependencies: queue-microtask: 1.2.3 + /sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + dependencies: + mri: 1.2.0 + dev: true + /safe-array-concat@1.0.1: resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} engines: {node: '>=0.4'} @@ -7280,6 +7357,11 @@ packages: engines: {node: '>=14.16'} dev: true + /type-fest@4.10.2: + resolution: {integrity: sha512-anpAG63wSpdEbLwOqH8L84urkL6PiVIov3EMmgIhhThevh9aiMQov+6Btx0wldNcvm4wV+e2/Rt1QdDwKHFbHw==} + engines: {node: '>=16'} + dev: true + /typed-array-buffer@1.0.0: resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} engines: {node: '>= 0.4'} diff --git a/src/cli/index.ts b/src/cli/index.ts new file mode 100644 index 00000000..4bdeab64 --- /dev/null +++ b/src/cli/index.ts @@ -0,0 +1,4 @@ +#!/usr/bin/env node + +import cli from '@gql.tada/cli-utils'; +cli(); diff --git a/tsconfig.json b/tsconfig.json index bea96a20..2fbdb6b5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,12 +1,15 @@ { "compilerOptions": { + "baseUrl": "./", + "rootDir": ".", + "paths": { + "@gql.tada/*": ["node_modules/@gql.tada/*/src", "packages/*/src"] + }, "forceConsistentCasingInFileNames": true, "importsNotUsedAsValues": "remove", "noEmit": true, "esModuleInterop": true, "noUnusedLocals": true, - "rootDir": ".", - "baseUrl": ".", "allowJs": true, "lib": ["dom", "esnext"], "jsx": "react", diff --git a/website/package.json b/website/package.json index 86e20497..b4df0c35 100644 --- a/website/package.json +++ b/website/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "astro dev", "start": "astro dev", - "build": "astro check && astro build", + "export": "astro check && astro build", "preview": "astro preview", "astro": "astro" },