Skip to content

Commit 7addeef

Browse files
sgulsethtzhelyazkova
andauthoredMar 19, 2024
feat(codegen): add groq finder methods. (#5980)
Adds methods that parses a js/ts source file and returns all groq queries. Co-authored-by: Tonina Zhelyazkova <zhelyazkova.tonina@gmail.com>
1 parent e94c02f commit 7addeef

14 files changed

+804
-17
lines changed
 
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"presets": [
3+
["@babel/preset-env", {"targets": "maintained node versions"}],
4+
[
5+
"@babel/preset-react",
6+
{
7+
"runtime": "automatic"
8+
}
9+
],
10+
"@babel/preset-typescript"
11+
]
12+
}

‎packages/@sanity/codegen/package.json

+17-1
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,24 @@
5757
"watch": "pkg-utils watch --tsconfig tsconfig.lib.json",
5858
"test": "jest"
5959
},
60-
"dependencies": {},
60+
"dependencies": {
61+
"@babel/core": "^7.23.9",
62+
"@babel/preset-env": "^7.23.8",
63+
"@babel/preset-react": "^7.23.3",
64+
"@babel/preset-typescript": "^7.23.3",
65+
"@babel/register": "^7.23.7",
66+
"@babel/traverse": "^7.23.5",
67+
"@babel/types": "^7.23.9",
68+
"debug": "^4.3.4",
69+
"tsconfig-paths": "^4.2.0"
70+
},
6171
"devDependencies": {
72+
"@jest/globals": "^29.7.0",
73+
"@types/babel__core": "^7.20.5",
74+
"@types/babel__register": "^7.17.3",
75+
"@types/babel__traverse": "^7.18.1",
76+
"@types/debug": "^4.1.12",
77+
"groq": "workspace:*",
6278
"rimraf": "^3.0.2"
6379
},
6480
"engines": {
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
export const TODO = 1
1+
export {findQueriesInSource} from '../typescript/findQueriesInSource'
2+
export {getResolver} from '../typescript/moduleResolver'
3+
export {registerBabel} from '../typescript/registerBabel'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import path from 'node:path'
2+
3+
import {describe, expect, test} from '@jest/globals'
4+
5+
import {findQueriesInSource} from '../findQueriesInSource'
6+
7+
describe('findQueries', () => {
8+
describe('should find queries in source', () => {
9+
test('plain string', () => {
10+
const source = `
11+
import { groq } from "groq";
12+
const postQuery = groq\`*[_type == "author"]\`
13+
const res = sanity.fetch(postQuery);
14+
`
15+
16+
const queries = findQueriesInSource(source, 'test.ts')
17+
const queryResult = queries[0]
18+
19+
expect(queryResult?.result).toEqual('*[_type == "author"]')
20+
})
21+
22+
test('with variables', () => {
23+
const source = `
24+
import { groq } from "groq";
25+
const type = "author";
26+
const authorQuery = groq\`*[_type == "\${type}"]\`
27+
const res = sanity.fetch(authorQuery);
28+
`
29+
30+
const queries = findQueriesInSource(source, 'test.ts')
31+
const queryResult = queries[0]
32+
33+
expect(queryResult?.result).toEqual('*[_type == "author"]')
34+
})
35+
36+
test('with function', () => {
37+
const source = `
38+
import { groq } from "groq";
39+
const getType = () => () => () => "author";
40+
const query = groq\`*[_type == "\${getType()()()}"]\`
41+
const res = sanity.fetch(query);
42+
`
43+
44+
const queries = findQueriesInSource(source, 'test.ts')
45+
46+
const queryResult = queries[0]
47+
48+
expect(queryResult?.result).toEqual('*[_type == "author"]')
49+
})
50+
51+
test('with block comment', () => {
52+
const source = `
53+
import { groq } from "groq";
54+
const type = "author";
55+
const query = /* groq */ groq\`*[_type == "\${type}"]\`;
56+
const res = sanity.fetch(query);
57+
`
58+
59+
const queries = findQueriesInSource(source, 'test.ts')
60+
const queryResult = queries[0]
61+
62+
expect(queryResult?.result).toEqual('*[_type == "author"]')
63+
})
64+
})
65+
66+
test('should not find inline queries in source', () => {
67+
const source = `
68+
import { groq } from "groq";
69+
const res = sanity.fetch(groq\`*[_type == "author"]\`);
70+
`
71+
72+
const queries = findQueriesInSource(source, 'test.ts')
73+
74+
expect(queries.length).toBe(0)
75+
})
76+
77+
test("should name queries with 'Result' at the end", () => {
78+
const source = `
79+
import { groq } from "groq";
80+
const postQuery = groq\`*[_type == "author"]\`
81+
const res = sanity.fetch(postQueryResult);
82+
`
83+
84+
const queries = findQueriesInSource(source, 'test.ts')
85+
const queryResult = queries[0]
86+
87+
expect(queryResult?.name.substr(-6)).toBe('Result')
88+
})
89+
90+
test('should import', () => {
91+
const source = `
92+
import { groq } from "groq";
93+
import {foo} from "./fixtures/exportVar";
94+
const postQuery = groq\`*[_type == "\${foo}"]\`
95+
const res = sanity.fetch(postQueryResult);
96+
`
97+
98+
const resolver: NodeJS.RequireResolve = (id) => {
99+
if (id === 'foo') {
100+
return path.resolve(__dirname, 'fixtures', 'exportVar')
101+
}
102+
return require.resolve(id)
103+
}
104+
resolver.paths = (request: string): string[] | null => {
105+
return require.resolve.paths(request)
106+
}
107+
108+
const queries = findQueriesInSource(source, 'test.ts', undefined, resolver)
109+
const queryResult = queries[0]
110+
111+
expect(queryResult?.name.substr(-6)).toBe('Result')
112+
})
113+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './exportVar'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const foo = 'foo'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import groq from 'groq'
2+
3+
export const postQuery = groq`*[_type == "author"]`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import groq from 'groq'
2+
3+
const postQuery = groq`*[_type == "author"]`

‎packages/@sanity/codegen/src/typescript/expressionResolvers.ts

+427
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import {createRequire} from 'node:module'
2+
import {join} from 'node:path'
3+
4+
import {type TransformOptions, traverse} from '@babel/core'
5+
import * as babelTypes from '@babel/types'
6+
7+
import {type NamedQueryResult, resolveExpression} from './expressionResolvers'
8+
import {parseSourceFile} from './parseSource'
9+
10+
const require = createRequire(__filename)
11+
12+
const groqTagName = 'groq'
13+
14+
const defaultBabelOptions = {
15+
extends: join(__dirname, '..', '..', 'babel.config.json'),
16+
}
17+
18+
/**
19+
* findQueriesInSource takes a source string and returns all GROQ queries in it.
20+
* @param source - The source code to search for queries
21+
* @param filename - The filename of the source code
22+
* @param babelConfig - The babel configuration to use when parsing the source
23+
* @param resolver - A resolver function to use when resolving module imports
24+
* @returns
25+
* @beta
26+
* @internal
27+
*/
28+
export function findQueriesInSource(
29+
source: string,
30+
filename: string,
31+
babelConfig: TransformOptions = defaultBabelOptions,
32+
resolver: NodeJS.RequireResolve = require.resolve,
33+
): NamedQueryResult[] {
34+
const queries: NamedQueryResult[] = []
35+
const file = parseSourceFile(source, filename, babelConfig)
36+
37+
traverse(file, {
38+
// Look for variable declarations, e.g. `const myQuery = groq`... and extract the query.
39+
// The variable name is used as the name of the query result type
40+
VariableDeclarator({node, scope}) {
41+
const init = node.init
42+
// Look for tagged template expressions that are called with the `groq` tag
43+
if (
44+
babelTypes.isTaggedTemplateExpression(init) &&
45+
babelTypes.isIdentifier(init.tag) &&
46+
babelTypes.isIdentifier(node.id) &&
47+
init.tag.name === groqTagName
48+
) {
49+
const queryName = `${node.id.name}Result`
50+
const queryResult = resolveExpression({
51+
node: init,
52+
file,
53+
scope,
54+
babelConfig,
55+
filename,
56+
resolver,
57+
})
58+
queries.push({name: queryName, result: queryResult})
59+
}
60+
},
61+
})
62+
63+
return queries
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import createDebug from 'debug'
2+
import {createMatchPath, loadConfig as loadTSConfig} from 'tsconfig-paths'
3+
4+
const debug = createDebug('sanity:codegen:moduleResolver')
5+
6+
/**
7+
* This is a custom implementation of require.resolve that takes into account the paths
8+
* configuration in tsconfig.json. This is necessary if we want to resolve paths that are
9+
* custom defined in the tsconfig.json file.
10+
* Resolving here is best effort and might not work in all cases.
11+
* @beta
12+
*/
13+
export function getResolver(cwd?: string): NodeJS.RequireResolve {
14+
const tsConfig = loadTSConfig(cwd)
15+
16+
if (tsConfig.resultType === 'failed') {
17+
debug('Could not load tsconfig, using default resolver: %s', tsConfig.message)
18+
return require.resolve
19+
}
20+
21+
const matchPath = createMatchPath(
22+
tsConfig.absoluteBaseUrl,
23+
tsConfig.paths,
24+
tsConfig.mainFields,
25+
tsConfig.addMatchAll,
26+
)
27+
28+
const resolve = function (request: string, options?: {paths?: string[]}): string {
29+
const found = matchPath(request)
30+
if (found !== undefined) {
31+
return require.resolve(found, options)
32+
}
33+
return require.resolve(request, options)
34+
}
35+
36+
// wrap the resolve.path function to make it available.
37+
resolve.paths = (request: string): string[] | null => {
38+
return require.resolve.paths(request)
39+
}
40+
return resolve
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import {parse, type TransformOptions} from '@babel/core'
2+
import type * as babelTypes from '@babel/types'
3+
4+
// helper function to parse a source file
5+
export function parseSourceFile(
6+
source: string,
7+
filename: string,
8+
babelOptions: TransformOptions,
9+
): babelTypes.File {
10+
const result = parse(source, {
11+
...babelOptions,
12+
filename,
13+
})
14+
15+
if (!result) {
16+
throw new Error(`Failed to parse ${filename}`)
17+
}
18+
19+
return result
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {join} from 'node:path'
2+
3+
import {type TransformOptions} from '@babel/core'
4+
import register from '@babel/register'
5+
6+
const defaultBabelOptions = {
7+
extends: join(__dirname, '..', '..', 'babel.config.json'),
8+
}
9+
10+
/**
11+
* Register Babel with the given options
12+
* @param babelOptions - The options to use when registering Babel
13+
* @beta
14+
*/
15+
export function registerBabel(babelOptions: TransformOptions = defaultBabelOptions): void {
16+
register({...babelOptions, extensions: ['.ts', '.tsx', '.js', '.jsx']})
17+
}

‎pnpm-lock.yaml

+82-15
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
Please sign in to comment.