Skip to content

Fix/nitro gql fragments #499

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nuxt-graphql-client",
"version": "0.2.34",
"version": "0.2.35",
"keywords": [
"vue",
"nuxt",
287 changes: 201 additions & 86 deletions src/module.ts
Original file line number Diff line number Diff line change
@@ -2,36 +2,51 @@ import { relative, resolve } from 'node:path'
import { existsSync, statSync } from 'fs'
import { defu } from 'defu'
import { upperFirst } from 'scule'
import { useLogger, addPlugin, addImportsDir, addTemplate, resolveFiles, createResolver, defineNuxtModule } from '@nuxt/kit'
import {
useLogger,
addPlugin,
addImportsDir,
addTemplate,
resolveFiles,
createResolver,
defineNuxtModule,
} from '@nuxt/kit'
import { name, version } from '../package.json'
import generate from './generate'
import { mapDocsToClients, extractGqlOperations } from './utils'
import type { GqlConfig, GqlClient, GqlCodegen, TokenStorageOpts } from './types'
import type {
GqlConfig,
GqlClient,
GqlCodegen,
TokenStorageOpts,
GqlError,
} from './types'
import { prepareContext, mockTemplate } from './context'
import type { GqlContext } from './context'

const logger = useLogger('nuxt-graphql-client')

export type ModuleOptions = Partial<GqlConfig>
export type { GqlError }

export default defineNuxtModule<GqlConfig>({
meta: {
name,
version,
configKey: 'graphql-client',
compatibility: {
nuxt: '^3.7.0'
}
nuxt: '^3.7.0',
},
},
defaults: {
clients: {},
watch: true,
codegen: true,
autoImport: true,
tokenStorage: true,
functionPrefix: 'Gql'
functionPrefix: 'Gql',
},
async setup (opts, nuxt) {
async setup(opts, nuxt) {
const resolver = createResolver(import.meta.url)
const srcResolver = createResolver(nuxt.options.srcDir)

@@ -41,7 +56,8 @@ export default defineNuxtModule<GqlConfig>({
{},
nuxt.options.runtimeConfig.public['graphql-client'],
nuxt.options.runtimeConfig.public.gql,
opts)
opts
)

const codegenDefaults: GqlCodegen = {
silent: true,
@@ -51,105 +67,161 @@ export default defineNuxtModule<GqlConfig>({
disableOnBuild: false,
onlyOperationTypes: true,
avoidOptionals: false,
maybeValue: 'T | null'
maybeValue: 'T | null',
}

config.codegen = !!config.codegen && defu<GqlCodegen, [GqlCodegen]>(config.codegen, codegenDefaults)

config.tokenStorage = !!config.tokenStorage && defu<TokenStorageOpts, [TokenStorageOpts]>(config.tokenStorage, {
mode: 'cookie',
cookieOptions: {
maxAge: 60 * 60 * 24 * 7,
secure: process.env.NODE_ENV === 'production'
}
})
config.codegen =
!!config.codegen &&
defu<GqlCodegen, [GqlCodegen]>(config.codegen, codegenDefaults)

config.tokenStorage =
!!config.tokenStorage &&
defu<TokenStorageOpts, [TokenStorageOpts]>(config.tokenStorage, {
mode: 'cookie',
cookieOptions: {
maxAge: 60 * 60 * 24 * 7,
secure: process.env.NODE_ENV === 'production',
},
})

const ctx: GqlContext = {
clientOps: {},
fnImports: [],
clients: Object.keys(config.clients!),
codegen: !config?.codegen ? false : !(!nuxt.options._prepare && !nuxt.options.dev) ? (nuxt.options._prepare || nuxt.options.dev) : !config?.codegen?.disableOnBuild
codegen: !config?.codegen
? false
: !(!nuxt.options._prepare && !nuxt.options.dev)
? nuxt.options._prepare || nuxt.options.dev
: !config?.codegen?.disableOnBuild,
}

if (!ctx?.clients?.length) {
const host =
process.env.GQL_HOST || nuxt.options.runtimeConfig.public.GQL_HOST

const clientHost =
process.env.GQL_CLIENT_HOST || nuxt.options.runtimeConfig.public.GQL_CLIENT_HOST
process.env.GQL_CLIENT_HOST ||
nuxt.options.runtimeConfig.public.GQL_CLIENT_HOST

if (!host) {
logger.warn('No GraphQL clients configured. Skipping module setup.')
return
}

ctx.clients = ['default']
config.clients = !clientHost ? { default: host } : { default: { host, clientHost } }
config.clients = !clientHost
? { default: host }
: { default: { host, clientHost } }
}

// @ts-ignore
nuxt.options.runtimeConfig['graphql-client'] = { clients: {} }
nuxt.options.runtimeConfig.public['graphql-client'] = defu(nuxt.options.runtimeConfig.public['graphql-client'], { clients: {} })
nuxt.options.runtimeConfig.public['graphql-client'] = defu(
nuxt.options.runtimeConfig.public['graphql-client'],
{ clients: {} }
)

const clientDefaults = {
token: { type: 'Bearer', name: 'Authorization' },
proxyCookies: true,
tokenStorage: config.tokenStorage,
preferGETQueries: config?.preferGETQueries ?? false
preferGETQueries: config?.preferGETQueries ?? false,
} as GqlClient<object>

const defaultClient = (config?.clients?.default && 'default') || Object.keys(config.clients!)[0]
const defaultClient =
(config?.clients?.default && 'default') || Object.keys(config.clients!)[0]

for (const [k, v] of Object.entries(config.clients!)) {
const conf = defu<GqlClient<object>, [GqlClient<object>]>(typeof v !== 'object'
? { host: v }
: { ...v, token: typeof v.token === 'string' ? { value: v.token } : v.token }, {
...clientDefaults,
...(typeof v === 'object' && typeof v.token !== 'string' && v?.token?.type === null && { token: { ...clientDefaults.token, type: null } })
})

const runtimeHost = k === defaultClient ? process.env.GQL_HOST : process.env?.[`GQL_${k.toUpperCase()}_HOST`]
if (runtimeHost) { conf.host = runtimeHost }
const conf = defu<GqlClient<object>, [GqlClient<object>]>(
typeof v !== 'object'
? { host: v }
: {
...v,
token: typeof v.token === 'string' ? { value: v.token } : v.token,
},
{
...clientDefaults,
...(typeof v === 'object' &&
typeof v.token !== 'string' &&
v?.token?.type === null && {
token: { ...clientDefaults.token, type: null },
}),
}
)

const runtimeHost =
k === defaultClient
? process.env.GQL_HOST
: process.env?.[`GQL_${k.toUpperCase()}_HOST`]
if (runtimeHost) {
conf.host = runtimeHost
}

const runtimeClientHost = k === defaultClient ? process.env.GQL_CLIENT_HOST : process.env?.[`GQL_${k.toUpperCase()}_CLIENT_HOST`]
if (runtimeClientHost) { conf.clientHost = runtimeClientHost }
const runtimeClientHost =
k === defaultClient
? process.env.GQL_CLIENT_HOST
: process.env?.[`GQL_${k.toUpperCase()}_CLIENT_HOST`]
if (runtimeClientHost) {
conf.clientHost = runtimeClientHost
}

if (!conf?.host) {
logger.warn(`GraphQL client (${k}) is missing it's host.`)
return
}

const runtimeToken = k === defaultClient ? process.env.GQL_TOKEN : process.env?.[`GQL_${k.toUpperCase()}_TOKEN`]
if (runtimeToken) { conf.token = { ...conf.token, value: runtimeToken } }
const runtimeToken =
k === defaultClient
? process.env.GQL_TOKEN
: process.env?.[`GQL_${k.toUpperCase()}_TOKEN`]
if (runtimeToken) {
conf.token = { ...conf.token, value: runtimeToken }
}

const runtimeTokenName = k === defaultClient ? process.env.GQL_TOKEN_NAME : process.env?.[`GQL_${k.toUpperCase()}_TOKEN_NAME`]
if (runtimeTokenName) { conf.token = { ...conf.token, name: runtimeTokenName } }
const runtimeTokenName =
k === defaultClient
? process.env.GQL_TOKEN_NAME
: process.env?.[`GQL_${k.toUpperCase()}_TOKEN_NAME`]
if (runtimeTokenName) {
conf.token = { ...conf.token, name: runtimeTokenName }
}

if (conf.tokenStorage) { conf.tokenStorage.name = conf.tokenStorage?.name || `gql:${k}` }
if (conf.tokenStorage) {
conf.tokenStorage.name = conf.tokenStorage?.name || `gql:${k}`
}

const schema = conf?.schema && srcResolver.resolve(conf.schema)

if (schema && !existsSync(schema)) {
delete conf.schema
logger.warn(`[nuxt-graphql-client] The Schema provided for the (${k}) GraphQL Client does not exist. \`host\` will be used as fallback.`)
logger.warn(
`[nuxt-graphql-client] The Schema provided for the (${k}) GraphQL Client does not exist. \`host\` will be used as fallback.`
)
}

ctx.clientOps![k] = []
config.clients![k] = JSON.parse(JSON.stringify(conf))
nuxt.options.runtimeConfig.public['graphql-client'].clients![k] = JSON.parse(JSON.stringify(conf))
nuxt.options.runtimeConfig.public['graphql-client'].clients![k] =
JSON.parse(JSON.stringify(conf))

if (conf?.token?.value) {
// @ts-ignore
nuxt.options.runtimeConfig['graphql-client'].clients[k] = { token: conf.token }
nuxt.options.runtimeConfig['graphql-client'].clients[k] = {
token: conf.token,
}

if (!conf?.retainToken) {
(nuxt.options.runtimeConfig.public['graphql-client'] as GqlConfig).clients![k].token!.value = undefined
;(
nuxt.options.runtimeConfig.public['graphql-client'] as GqlConfig
).clients![k].token!.value = undefined
}
}
}

// Resolve all document path layers that extend the default layer
const documentPaths = nuxt.options._layers.map(layer => layer.config.srcDir)
const documentPaths = nuxt.options._layers.map(
(layer) => layer.config.srcDir
)

if (config.documentPaths) {
for (const path of config.documentPaths) {
@@ -164,10 +236,14 @@ export default defineNuxtModule<GqlConfig>({
}

const gqlMatch = '**/*.{gql,graphql}'
async function generateGqlTypes (hmrDoc?: string) {
async function generateGqlTypes(hmrDoc?: string) {
const documents: string[] = []
for await (const path of documentPaths) {
const files = (await resolveFiles(path, [gqlMatch, '!**/schemas'], { followSymbolicLinks: false })).filter(allowDocument)
const files = (
await resolveFiles(path, [gqlMatch, '!**/schemas'], {
followSymbolicLinks: false,
})
).filter(allowDocument)

documents.push(...files)
}
@@ -182,26 +258,41 @@ export default defineNuxtModule<GqlConfig>({
if (ctx.clientDocs) {
const clientDocs = !hmrDoc
? ctx.clientDocs
: Object.keys(ctx.clientDocs)
.filter(k => ctx.clientDocs?.[k]?.some(e => e.endsWith(hmrDoc)))
.reduce((acc, k) => ({ ...acc, [k]: ctx.clientDocs?.[k] }), {}) as Record<string, string[]>
: (Object.keys(ctx.clientDocs)
.filter((k) =>
ctx.clientDocs?.[k]?.some((e) => e.endsWith(hmrDoc))
)
.reduce(
(acc, k) => ({ ...acc, [k]: ctx.clientDocs?.[k] }),
{}
) as Record<string, string[]>)

const codegenResult = ctx?.codegen
? await generate({
clients: config.clients as GqlConfig['clients'],
plugins,
documents,
resolver: srcResolver,
clientDocs,
...(typeof config.codegen !== 'boolean' && config.codegen)
}).then(output => output.reduce<Record<string, string>>((acc, c) => ({ ...acc, [c.filename.split('.ts')[0]]: c.content }), {}))
clients: config.clients as GqlConfig['clients'],
plugins,
documents,
resolver: srcResolver,
clientDocs,
...(typeof config.codegen !== 'boolean' && config.codegen),
}).then((output) =>
output.reduce<Record<string, string>>(
(acc, c) => ({
...acc,
[c.filename.split('.ts')[0]]: c.content,
}),
{}
)
)
: ctx.clients!.reduce<Record<string, string>>((acc, k) => {
if (!clientDocs?.[k]?.length) { return acc }
if (!clientDocs?.[k]?.length) {
return acc
}

const entries = extractGqlOperations(ctx?.clientDocs?.[k] || [])
const entries = extractGqlOperations(ctx?.clientDocs?.[k] || [])

return { ...acc, [k]: mockTemplate(entries) }
}, {})
return { ...acc, [k]: mockTemplate(entries) }
}, {})

ctx.template = defu(codegenResult, ctx.template)
}
@@ -212,24 +303,31 @@ export default defineNuxtModule<GqlConfig>({
addPlugin(resolver.resolve('runtime/plugin'))

if (config.autoImport) {
nuxt.options.alias['#gql'] = resolver.resolve(nuxt.options.buildDir, 'gql')
nuxt.options.alias['#gql/*'] = resolver.resolve(nuxt.options.buildDir, 'gql', '*')
nuxt.options.alias['#gql'] = resolver.resolve(
nuxt.options.buildDir,
'gql'
)
nuxt.options.alias['#gql/*'] = resolver.resolve(
nuxt.options.buildDir,
'gql',
'*'
)

addTemplate({
filename: 'gql.mjs',
getContents: () => ctx.generateImports?.() || ''
getContents: () => ctx.generateImports?.() || '',
})

addTemplate({
filename: 'gql/index.d.ts',
getContents: () => ctx.generateDeclarations?.() || ''
getContents: () => ctx.generateDeclarations?.() || '',
})

for (const client of ctx.clients) {
addTemplate({
write: ctx.codegen,
filename: `gql/${client}.${ctx.codegen ? 'ts' : 'mjs'}`,
getContents: () => ctx.template?.[client] || ''
getContents: () => ctx.template?.[client] || '',
})
}

@@ -241,55 +339,72 @@ export default defineNuxtModule<GqlConfig>({
}

nuxt.hook('nitro:config', (nitro) => {
if (nitro.imports === false) { return }
if (nitro.imports === false) {
return
}

nitro.externals = nitro.externals || {}
nitro.externals.inline = nitro.externals.inline || []
nitro.externals.inline.push(resolver.resolve('runtime'))

const clientSdks = Object.entries(ctx.clientDocs || {}).reduce<string[]>((acc, [client, docs]) => {
const entries = extractGqlOperations(docs)

return [...acc, `${client}: ` + mockTemplate(entries).replace('export ', '')]
}, [])

nitro.virtual = nitro.virtual || {}
nitro.virtual['#gql-nitro'] = [
'const clientSdks = {' + clientSdks + '}',
"import { getSdk } from '#gql/default'",
'const config = ' + JSON.stringify(config.clients),
'const ops = ' + JSON.stringify(ctx.clientOps),
'const clients = {}',
'const useGql = (op, variables = undefined) => {',
' const client = Object.keys(ops).find(k => ops[k].includes(op))',
' return clientSdks[client](clients?.[client])[op](variables)',
'const useGql = (op, ...params) => {',
" const key = Object.keys(ops).find(k => ops[k].includes(op)) || 'default'",
' const client = clients[key]',
' const sdk = getSdk(client)',
' const document = sdk[op]',
' return document(...params)',
'}',
ctx.fns?.map(fn => `export const ${config.functionPrefix + upperFirst(fn)} = (...params) => useGql('${fn}', ...params)`).join('\n'),
'export default { clients, config }'
ctx.fns
?.map(
(fn) =>
`export const ${
config.functionPrefix + upperFirst(fn)
} = (...params) => useGql('${fn}', ...params)`
)
.join('\n'),
'export default { clients, config }',
].join('\n')

nitro.imports = defu(nitro.imports, {
presets: [{
from: '#gql-nitro',
imports: ctx.fns?.map(fn => config.functionPrefix + upperFirst(fn))
}]
presets: [
{
from: '#gql-nitro',
imports: ctx.fns?.map(
(fn) => config.functionPrefix + upperFirst(fn)
),
},
],
})

nitro.plugins = nitro.plugins || []
nitro.plugins.push(resolver.resolve('runtime/nitro'))
})

const allowDocument = (f: string) => {
const isSchema = f.match(/([^/]+)\.(gql|graphql)$/)?.[0]?.toLowerCase().includes('schema')
const isSchema = f
.match(/([^/]+)\.(gql|graphql)$/)?.[0]
?.toLowerCase()
.includes('schema')

return !isSchema && !!statSync(srcResolver.resolve(f)).size
}

if (config.watch) {
nuxt.hook('builder:watch', async (event, path) => {
path = relative(nuxt.options.srcDir, resolve(nuxt.options.srcDir, path))
if (!path.match(/\.(gql|graphql)$/)) { return }
if (!path.match(/\.(gql|graphql)$/)) {
return
}

if (event !== 'unlink' && !allowDocument(path)) { return }
if (event !== 'unlink' && !allowDocument(path)) {
return
}

const start = Date.now()
await generateGqlTypes(path)
@@ -309,7 +424,7 @@ export default defineNuxtModule<GqlConfig>({
delete config.define['typeof document']
}
})
}
},
})

declare module '@nuxt/schema' {