Skip to content

Commit

Permalink
feat(codegen): generate SanityQueries interface in @sanity/codegen (#…
Browse files Browse the repository at this point in the history
…6997) (#7304)

* fix(deps): update dependency @sanity/client to ^6.21.1 (#7215)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* feat(typegen): groq/define module with a defineQuery helper

* feat(typegen): Recognize queries that are wrapped in defineQuery calls

* feat(typegen): Add overloadClientMethods option to @sanity/codegen

* refactor(codegen): Return the typename of a generated type node so we can store it for later use

* feat(typegen): Add generateTypeMap helper to TypeGenerator

* feat(typegen): Allow codegen cli to generate the SanityQueries type map

* docs(typegen): Add docs about defineQuery to groq README

* refactor(groq): Allow defineQuery to live in the groq package

* refactor(groq): Use the correct groq import in codegen tests

* fix(codegen): Only use defineQuery when it comes from the groq package

* refactor(codegen): Avoid changing the signature for generateTypeNodeTypes

* refactor(codegen): Use typeNode to reference types in typeGenerator

* feat(codegen): Handle duplicate query strings

---------

Co-authored-by: Romeo Van Snick <romeo@romeovansnick.be>
  • Loading branch information
stipsan and romeovs committed Aug 3, 2024
1 parent fe40caf commit 886ab25
Show file tree
Hide file tree
Showing 17 changed files with 586 additions and 39 deletions.
44 changes: 26 additions & 18 deletions packages/@sanity/cli/src/actions/typegen/generateAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export default async function typegenGenerateAction(
workDir,
schemaPath: codegenConfig.schema,
searchPath: codegenConfig.path,
overloadClientMethods: codegenConfig.overloadClientMethods,
prettierConfig,
} satisfies TypegenGenerateTypesWorkerData,
// eslint-disable-next-line no-process-env
Expand Down Expand Up @@ -131,25 +132,32 @@ export default async function typegenGenerateAction(
return
}

stats.queryFilesCount++
for (const {
queryName,
query,
type,
typeNodesGenerated,
unknownTypeNodesGenerated,
emptyUnionTypeNodesGenerated,
} of msg.types) {
fileTypeString += `// Variable: ${queryName}\n`
fileTypeString += `// Query: ${query.replace(/(\r\n|\n|\r)/gm, '')}\n`
fileTypeString += type
stats.queriesCount++
stats.typeNodesGenerated += typeNodesGenerated
stats.unknownTypeNodesGenerated += unknownTypeNodesGenerated
stats.emptyUnionTypeNodesGenerated += emptyUnionTypeNodesGenerated
if (msg.type === 'types') {
stats.queryFilesCount++
for (const {
queryName,
query,
type,
typeNodesGenerated,
unknownTypeNodesGenerated,
emptyUnionTypeNodesGenerated,
} of msg.types) {
fileTypeString += `// Variable: ${queryName}\n`
fileTypeString += `// Query: ${query.replace(/(\r\n|\n|\r)/gm, '')}\n`
fileTypeString += type
stats.queriesCount++
stats.typeNodesGenerated += typeNodesGenerated
stats.unknownTypeNodesGenerated += unknownTypeNodesGenerated
stats.emptyUnionTypeNodesGenerated += emptyUnionTypeNodesGenerated
}
typeFile.write(fileTypeString)
stats.size += Buffer.byteLength(fileTypeString)
}

if (msg.type === 'typemap') {
typeFile.write(msg.typeMap)
stats.size += Buffer.byteLength(msg.typeMap)
}
typeFile.write(fileTypeString)
stats.size += Buffer.byteLength(fileTypeString)
})
worker.addListener('error', reject)
})
Expand Down
28 changes: 23 additions & 5 deletions packages/@sanity/cli/src/workers/typegenGenerate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface TypegenGenerateTypesWorkerData {
schemaPath: string
searchPath: string | string[]
prettierConfig: PrettierOptions | null
overloadClientMethods?: boolean
}

export type TypegenGenerateTypesWorkerMessage =
Expand Down Expand Up @@ -49,6 +50,11 @@ export type TypegenGenerateTypesWorkerMessage =
schema: string
length: number
}
| {
type: 'typemap'
filename: string
typeMap: string
}
| {
type: 'complete'
}
Expand Down Expand Up @@ -115,6 +121,8 @@ async function main() {
queryName: string
query: string
type: string
typeName: string
typeNode: TypeNode
unknownTypeNodesGenerated: number
typeNodesGenerated: number
emptyUnionTypeNodesGenerated: number
Expand All @@ -124,16 +132,17 @@ async function main() {
const ast = safeParseQuery(query)
const queryTypes = typeEvaluate(ast, schema)

const type = await maybeFormatCode(
typeGenerator.generateTypeNodeTypes(`${queryName}Result`, queryTypes).trim(),
opts.prettierConfig,
)
const typeName = `${queryName}Result`
const type = typeGenerator.generateTypeNodeTypes(typeName, queryTypes)
const code = await maybeFormatCode(type.trim(), opts.prettierConfig)

const queryTypeStats = walkAndCountQueryTypeNodeStats(queryTypes)
fileQueryTypes.push({
queryName,
query,
type,
typeName,
typeNode: queryTypes,
type: code,
unknownTypeNodesGenerated: queryTypeStats.unknownTypes,
typeNodesGenerated: queryTypeStats.allTypes,
emptyUnionTypeNodesGenerated: queryTypeStats.emptyUnions,
Expand All @@ -159,6 +168,15 @@ async function main() {
filename: result.filename,
} satisfies TypegenGenerateTypesWorkerMessage)
}

if (fileQueryTypes.length > 0 && opts.overloadClientMethods) {
const typeMap = typeGenerator.generateQueryMap(fileQueryTypes)
parentPort?.postMessage({
type: 'typemap',
filename: result.filename,
typeMap,
} satisfies TypegenGenerateTypesWorkerMessage)
}
}

parentPort?.postMessage({
Expand Down
1 change: 1 addition & 0 deletions packages/@sanity/codegen/src/readConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const configDefintion = z.object({
schema: z.string().default('./schema.json'),
generates: z.string().default('./sanity.types.ts'),
formatGeneratedCode: z.boolean().default(true),
overloadClientMethods: z.boolean().default(false),
})

export type CodegenConfig = z.infer<typeof configDefintion>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {describe, expect, test} from '@jest/globals'

import {findQueriesInSource} from '../findQueriesInSource'

describe('findQueries', () => {
describe('findQueries with the groq template', () => {
describe('should find queries in source', () => {
test('plain string', () => {
const source = `
Expand Down Expand Up @@ -157,3 +157,201 @@ describe('findQueries', () => {
expect(queries.length).toBe(0)
})
})

describe('findQueries with defineQuery', () => {
describe('should find queries in source', () => {
test('plain string', () => {
const source = `
import { defineQuery } from "groq";
const postQuery = defineQuery("*[_type == 'author']");
const res = sanity.fetch(postQuery);
`

const queries = findQueriesInSource(source, 'test.ts')
const queryResult = queries[0]

expect(queryResult?.result).toEqual("*[_type == 'author']")
})

test('template string', () => {
const source = `
import { defineQuery } from "groq";
const postQuery = defineQuery(\`*[_type == "author"]\`);
const res = sanity.fetch(postQuery);
`

const queries = findQueriesInSource(source, 'test.ts')
const queryResult = queries[0]

expect(queryResult?.result).toEqual('*[_type == "author"]')
})

test('with variables', () => {
const source = `
import { defineQuery } from "groq";
const type = "author";
const authorQuery = defineQuery(\`*[_type == "\${type}"]\`);
const res = sanity.fetch(authorQuery);
`

const queries = findQueriesInSource(source, 'test.ts')
const queryResult = queries[0]

expect(queryResult?.result).toEqual('*[_type == "author"]')
})

test('with function', () => {
const source = `
import { defineQuery } from "groq";
const getType = () => () => () => "author";
const query = defineQuery(\`*[_type == "\${getType()()()}"]\`);
const res = sanity.fetch(query);
`

const queries = findQueriesInSource(source, 'test.ts')

const queryResult = queries[0]

expect(queryResult?.result).toEqual('*[_type == "author"]')
})

test('with block comment', () => {
const source = `
import { defineQuery } from "groq";
const type = "author";
const query = /* groq */ defineQuery(\`*[_type == "\${type}"]\`);
const res = sanity.fetch(query);
`

const queries = findQueriesInSource(source, 'test.ts')
const queryResult = queries[0]

expect(queryResult?.result).toEqual('*[_type == "author"]')
})
})

test('should not find inline queries in source', () => {
const source = `
import { defineQuery } from "groq";
const res = sanity.fetch(defineQuery(\`*[_type == "author"]\`));
`

const queries = findQueriesInSource(source, 'test.ts')

expect(queries.length).toBe(0)
})

test('should import', () => {
const source = `
import {defineQuery} from "groq";
import {foo} from "./fixtures/exportVar";
const postQuery = defineQuery(\`*[_type == "\${foo}"]\`);
const res = sanity.fetch(postQueryResult);
`
const queries = findQueriesInSource(source, __filename, undefined)
expect(queries.length).toBe(1)
expect(queries[0].result).toBe('*[_type == "foo"]')
})

test('should import, subdirectory', () => {
const source = `
import {defineQuery} from "groq";
import {foo} from "../__tests__/fixtures/exportVar";
const postQuery = defineQuery(\`*[_type == "\${foo}"]\`);
const res = sanity.fetch(postQueryResult);
`
const queries = findQueriesInSource(source, __filename, undefined)
expect(queries.length).toBe(1)
expect(queries[0].result).toBe('*[_type == "foo"]')
})

test('can import sequence of files', () => {
const source = `
import {defineQuery} from "groq";
import {query} from "../__tests__/fixtures/importSeq1";
const someQuery = defineQuery(\`$\{query}\`);
`
const queries = findQueriesInSource(source, __filename, undefined)
expect(queries.length).toBe(1)
expect(queries[0].result).toBe('*[_type == "foo bar"]')
})

test('should detect defineQuery calls that have been required', () => {
const source = `
const {defineQuery} = require("groq");
import {query} from "../__tests__/fixtures/importSeq1";
const someQuery = defineQuery(\`$\{query}\`);
`
const queries = findQueriesInSource(source, __filename, undefined)
expect(queries.length).toBe(1)
expect(queries[0].result).toBe('*[_type == "foo bar"]')
})

test('will ignore declarations with ignore tag', () => {
const source = `
import {defineQuery} from "groq";
// @sanity-typegen-ignore
const postQuery = defineQuery(\`*[_type == "foo"]\`);
`

const queries = findQueriesInSource(source, __filename, undefined)
expect(queries.length).toBe(0)
})

test('will ignore export named declarations with ignore tag', () => {
const source = `
import {defineQuery} from "groq";
// @sanity-typegen-ignore
export const postQuery = defineQuery(\`*[_type == "foo"]\`);
`

const queries = findQueriesInSource(source, __filename, undefined)
expect(queries.length).toBe(0)
})

test('will ignore declarations with ignore tag, even with multiple comments above declaration', () => {
const source = `
import {defineQuery} from "groq";
// This is a query that queries posts
// @sanity-typegen-ignore
export const postQuery = groq\`*[_type == "foo"]\`
`

const queries = findQueriesInSource(source, __filename, undefined)
expect(queries.length).toBe(0)
})

test('will ignore declerations if any of the leading comments are ignore tags', () => {
const source = `
import {defineQuery} from "groq";
// @sanity-typegen-ignore
// This should be ignored because of the comment above
export const postQuery = defineQuery(\`*[_type == "foo"]\`);
`

const queries = findQueriesInSource(source, __filename, undefined)
expect(queries.length).toBe(0)
})

test('will ignore defineQuery calls that are not coming from the groq module', () => {
const source = `
import {defineQuery} from "another-module";
export const postQuery = defineQuery(\`*[_type == "foo"]\`);
`
const queries = findQueriesInSource(source, __filename, undefined)
expect(queries.length).toBe(0)
})

test('will ignore defineQuery calls that are not coming from the groq module when using require', () => {
const source = `
const {defineQuery} = require("another-module");
export const postQuery = defineQuery(\`*[_type == "foo"]\`);
`
const queries = findQueriesInSource(source, __filename, undefined)
expect(queries.length).toBe(0)
})
})
Loading

0 comments on commit 886ab25

Please sign in to comment.