Skip to content

Commit

Permalink
feat(core): support generating the tada files without the LSP active (#…
Browse files Browse the repository at this point in the history
…146)

* support generating the tada files without the LSP active

* increase timeout for CI environments
  • Loading branch information
JoviDeCroock committed Feb 6, 2024
1 parent 4b8f627 commit b3f7933
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 92 deletions.
5 changes: 5 additions & 0 deletions .changeset/violet-cheetahs-arrive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'fuse': minor
---

Ensure we generate `tada` in IDE's where the `tsserver` might not be supported
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"@pothos/plugin-relay": "^3.44.0",
"@pothos/plugin-scope-auth": "^3.20.0",
"@urql/core": "^4.2.3",
"@urql/introspection": "^1.0.3",
"@urql/next": "^1.1.1",
"aws-lambda": "^1.0.7",
"dataloader": "^2.2.2",
Expand Down
101 changes: 23 additions & 78 deletions packages/core/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import { existsSync } from 'fs'
import fs from 'fs/promises'
import { createServer, build } from 'vite'
import { VitePluginNode } from 'vite-plugin-node'
import { generate, CodegenContext } from '@graphql-codegen/cli'
import { DateTimeResolver, JSONResolver } from 'graphql-scalars'

import { isUsingGraphQLTada, tadaGqlContents } from './utils/gql-tada'
import {
ensureTadaIntrospection,
isUsingGraphQLTada,
tadaGqlContents,
} from './utils/gql-tada'
import { boostrapCodegen } from './utils/codegen'

const prog = sade('fuse')

Expand Down Expand Up @@ -144,6 +147,7 @@ prog

const baseDirectory = process.cwd()
const isUsingTada = opts.client && (await isUsingGraphQLTada(baseDirectory))
let hasTadaWatcherRunning = false

if (opts.server) {
let yoga
Expand All @@ -157,7 +161,12 @@ prog
path.resolve(baseDirectory, 'schema.graphql'),
yo.stringifiedSchema,
'utf-8',
)
).then(() => {
if (isUsingTada && !hasTadaWatcherRunning) {
hasTadaWatcherRunning = true
ensureTadaIntrospection(baseDirectory, true)
}
})

return yo
})
Expand All @@ -180,8 +189,8 @@ prog
if (isUsingTada) {
setTimeout(() => {
fetch(
`http://localhost:${opts.port}/api/graphql?query={__typename}`,
)
`http://localhost:${opts.port}/graphql?query={__typename}`,
).catch(() => {})
}, 500)
}
server.restart()
Expand All @@ -195,15 +204,17 @@ prog
if (opts.client) {
if (!isUsingTada) {
setTimeout(() => {
fetch(
`http://localhost:${opts.port}/api/graphql?query={__typename}`,
).then(() => {
boostrapCodegen(opts.schema, true)
})
fetch(`http://localhost:${opts.port}/graphql?query={__typename}`)
.then(() => {
boostrapCodegen(opts.schema, true)
})
.catch(() => {})
}, 1000)
} else {
setTimeout(() => {
fetch(`http://localhost:${opts.port}/api/graphql?query={__typename}`)
fetch(
`http://localhost:${opts.port}/graphql?query={__typename}`,
).catch(() => {})
}, 1000)
const hasSrcDir = existsSync(path.resolve(baseDirectory, 'src'))
const base = hasSrcDir
Expand All @@ -226,69 +237,3 @@ prog
})

prog.parse(process.argv)

async function boostrapCodegen(location: string, watch: boolean) {
const baseDirectory = process.cwd()
const hasSrcDir = existsSync(path.resolve(baseDirectory, 'src'))

const contents = `export * from "./fragment-masking";
export * from "./gql";
export * from "fuse/client";\n`
const ctx = new CodegenContext({
filepath: 'codgen.yml',
config: {
ignoreNoDocuments: true,
errorsOnly: true,
noSilentErrors: true,
hooks: {
afterOneFileWrite: async () => {
await fs.writeFile(
hasSrcDir
? baseDirectory + '/src/fuse/index.ts'
: baseDirectory + '/fuse/index.ts',
contents,
)
},
},
watch: watch
? [
hasSrcDir
? baseDirectory + '/src/**/*.{ts,tsx}'
: baseDirectory + '/**/*.{ts,tsx}',
'!./{node_modules,.next,.git}/**/*',
hasSrcDir ? '!./src/fuse/*.{ts,tsx}' : '!./fuse/*.{ts,tsx}',
]
: false,
schema: location,
generates: {
[hasSrcDir ? baseDirectory + '/src/fuse/' : baseDirectory + '/fuse/']: {
documents: [
hasSrcDir ? './src/**/*.{ts,tsx}' : './**/*.{ts,tsx}',
'!./{node_modules,.next,.git}/**/*',
hasSrcDir ? '!./src/fuse/*.{ts,tsx}' : '!./fuse/*.{ts,tsx}',
],
preset: 'client',
// presetConfig: {
// persistedDocuments: true,
// },
config: {
scalars: {
ID: {
input: 'string',
output: 'string',
},
DateTime: DateTimeResolver.extensions.codegenScalarType,
JSON: JSONResolver.extensions.codegenScalarType,
},
avoidOptionals: false,
enumsAsTypes: true,
nonOptionalTypename: true,
skipTypename: false,
},
},
},
},
})

await generate(ctx, true)
}
70 changes: 70 additions & 0 deletions packages/core/src/utils/codegen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { generate, CodegenContext } from '@graphql-codegen/cli'
import { DateTimeResolver, JSONResolver } from 'graphql-scalars'
import { existsSync, promises as fs } from 'fs'
import path from 'path'

export async function boostrapCodegen(location: string, watch: boolean) {
const baseDirectory = process.cwd()
const hasSrcDir = existsSync(path.resolve(baseDirectory, 'src'))

const contents = `export * from "./fragment-masking";
export * from "./gql";
export * from "fuse/client";\n`
const ctx = new CodegenContext({
filepath: 'codgen.yml',
config: {
ignoreNoDocuments: true,
errorsOnly: true,
noSilentErrors: true,
hooks: {
afterOneFileWrite: async () => {
await fs.writeFile(
hasSrcDir
? baseDirectory + '/src/fuse/index.ts'
: baseDirectory + '/fuse/index.ts',
contents,
)
},
},
watch: watch
? [
hasSrcDir
? baseDirectory + '/src/**/*.{ts,tsx}'
: baseDirectory + '/**/*.{ts,tsx}',
'!./{node_modules,.next,.git}/**/*',
hasSrcDir ? '!./src/fuse/*.{ts,tsx}' : '!./fuse/*.{ts,tsx}',
]
: false,
schema: location,
generates: {
[hasSrcDir ? baseDirectory + '/src/fuse/' : baseDirectory + '/fuse/']: {
documents: [
hasSrcDir ? './src/**/*.{ts,tsx}' : './**/*.{ts,tsx}',
'!./{node_modules,.next,.git}/**/*',
hasSrcDir ? '!./src/fuse/*.{ts,tsx}' : '!./fuse/*.{ts,tsx}',
],
preset: 'client',
// presetConfig: {
// persistedDocuments: true,
// },
config: {
scalars: {
ID: {
input: 'string',
output: 'string',
},
DateTime: DateTimeResolver.extensions.codegenScalarType,
JSON: JSONResolver.extensions.codegenScalarType,
},
avoidOptionals: false,
enumsAsTypes: true,
nonOptionalTypename: true,
skipTypename: false,
},
},
},
},
})

await generate(ctx, true)
}
83 changes: 82 additions & 1 deletion packages/core/src/utils/gql-tada.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { promises as fs } from 'fs'
import { promises as fs, watch, existsSync } from 'fs'
import path from 'path'
import { buildSchema, introspectionFromSchema } from 'graphql'
import { minifyIntrospectionQuery } from '@urql/introspection'

export async function isUsingGraphQLTada(cwd: string): Promise<boolean> {
const [pkgJson, tsConfig] = await Promise.allSettled([
Expand Down Expand Up @@ -63,3 +65,82 @@ 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')
33 changes: 20 additions & 13 deletions packages/core/test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const wait = (timeout = 1000) => {
describe.each(allFixtures)('%s', (fixtureName) => {
const fixtureDir = path.join(fixturesDir, fixtureName)
let process: ExecaChildProcess<string> | undefined
let basePort = fixtureName === 'tada' ? 4000 : 5000

beforeAll(async () => {
await execa('pnpm', ['install'], { cwd: fixtureDir })
Expand Down Expand Up @@ -49,7 +50,8 @@ describe.each(allFixtures)('%s', (fixtureName) => {
})

test('Should run the dev command', async () => {
process = execa('pnpm', ['fuse', 'dev', '--server'], {
const port = '' + basePort
process = execa('pnpm', ['fuse', 'dev', '--server', '--port', port], {
cwd: fixtureDir,
})

Expand All @@ -62,7 +64,7 @@ describe.each(allFixtures)('%s', (fixtureName) => {
})
})

const result = await fetch('http://localhost:4000/graphql', {
const result = await fetch(`http://localhost:${port}/graphql`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand All @@ -76,32 +78,37 @@ describe.each(allFixtures)('%s', (fixtureName) => {

if (fixtureName === 'tada') {
test('Should run the client dev command', async () => {
process = execa('pnpm', ['fuse', 'dev'], {
process = execa('pnpm', ['fuse', 'dev', '--port', '' + basePort + 1], {
cwd: fixtureDir,
})

await new Promise((resolve) => {
process!.stdout?.on('data', (data) => {
const msg = data.toString()
console.log(msg)
if (msg.includes('Server listening on')) {
resolve(null)
}
})
})

await wait()
// We have a timeout internally to generate the schema of 1 second
await new Promise((resolve) => {
let interval = setInterval(() => {
if (fs.existsSync(path.join(fixtureDir, 'schema.graphql'))) {
clearInterval(interval)
resolve(null)
}
}, 500)
})

expect(existsSync(path.join(fixtureDir, 'fuse'))).toBe(true)
// TODO: the TS-LSP does not work in this process, we might want to
// add a safeguard to the client dev command to ensure that the
// types can work even if the LSP is not running.
//expect(
// existsSync(path.join(fixtureDir, 'fuse', 'introspection.ts')),
//).toBe(true)
//expect(existsSync(path.join(fixtureDir, 'fuse', 'tada.ts'))).toBe(true)
expect(existsSync(path.join(fixtureDir, 'fuse', 'tada.ts'))).toBe(true)
expect(existsSync(path.join(fixtureDir, 'fuse', 'index.ts'))).toBe(true)
}, 10_000)
await wait(1000)
expect(
existsSync(path.join(fixtureDir, 'fuse', 'introspection.ts')),
).toBe(true)
}, 15_000)
}

test('Should run the build command', async () => {
Expand Down
Loading

0 comments on commit b3f7933

Please sign in to comment.