diff --git a/packages/@sanity/cli/src/actions/typegen/generateAction.ts b/packages/@sanity/cli/src/actions/typegen/generateAction.ts index 7127d0c2d33..da4def4a376 100644 --- a/packages/@sanity/cli/src/actions/typegen/generateAction.ts +++ b/packages/@sanity/cli/src/actions/typegen/generateAction.ts @@ -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 @@ -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) }) diff --git a/packages/@sanity/cli/src/workers/typegenGenerate.ts b/packages/@sanity/cli/src/workers/typegenGenerate.ts index 642765e3770..5c0cad9839d 100644 --- a/packages/@sanity/cli/src/workers/typegenGenerate.ts +++ b/packages/@sanity/cli/src/workers/typegenGenerate.ts @@ -21,6 +21,7 @@ export interface TypegenGenerateTypesWorkerData { schemaPath: string searchPath: string | string[] prettierConfig: PrettierOptions | null + overloadClientMethods?: boolean } export type TypegenGenerateTypesWorkerMessage = @@ -49,6 +50,11 @@ export type TypegenGenerateTypesWorkerMessage = schema: string length: number } + | { + type: 'typemap' + filename: string + typeMap: string + } | { type: 'complete' } @@ -115,6 +121,8 @@ async function main() { queryName: string query: string type: string + typeName: string + typeNode: TypeNode unknownTypeNodesGenerated: number typeNodesGenerated: number emptyUnionTypeNodesGenerated: number @@ -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, @@ -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({ diff --git a/packages/@sanity/codegen/src/readConfig.ts b/packages/@sanity/codegen/src/readConfig.ts index ff9ffeb4edb..082130b56f8 100644 --- a/packages/@sanity/codegen/src/readConfig.ts +++ b/packages/@sanity/codegen/src/readConfig.ts @@ -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 diff --git a/packages/@sanity/codegen/src/typescript/__tests__/findQueriesInSource.test.ts b/packages/@sanity/codegen/src/typescript/__tests__/findQueriesInSource.test.ts index 33a25f36500..a8acc198659 100644 --- a/packages/@sanity/codegen/src/typescript/__tests__/findQueriesInSource.test.ts +++ b/packages/@sanity/codegen/src/typescript/__tests__/findQueriesInSource.test.ts @@ -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 = ` @@ -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) + }) +}) diff --git a/packages/@sanity/codegen/src/typescript/__tests__/typeGenerator.test.ts b/packages/@sanity/codegen/src/typescript/__tests__/typeGenerator.test.ts index 6e99adb5154..6f312be88a9 100644 --- a/packages/@sanity/codegen/src/typescript/__tests__/typeGenerator.test.ts +++ b/packages/@sanity/codegen/src/typescript/__tests__/typeGenerator.test.ts @@ -455,3 +455,97 @@ export type AllSanitySchemaTypes = OptionalData;" expect(objectNodeOut).toMatchSnapshot() }) }) + +describe('generateQueryMap', () => { + test('should generate a map of query results', () => { + const schema: SchemaType = [] + + const queries = [ + { + typeNode: {type: 'unknown'} satisfies TypeNode, + query: '*[_type == "author"]', + }, + { + typeNode: {type: 'unknown'} satisfies TypeNode, + query: '*[_type == "author"][0]', + }, + ] + + const typeGenerator = new TypeGenerator(schema) + typeGenerator.generateTypeNodeTypes('AuthorsResult', queries[0].typeNode) + typeGenerator.generateTypeNodeTypes('FirstAuthorResult', queries[1].typeNode) + + const actualOutput = typeGenerator.generateQueryMap(queries) + + expect(actualOutput).toMatchInlineSnapshot(` +"import \\"@sanity/client\\"; +declare module \\"@sanity/client\\" { + interface SanityQueries { + \\"*[_type == \\\\\\"author\\\\\\"]\\": AuthorsResult; + \\"*[_type == \\\\\\"author\\\\\\"][0]\\": FirstAuthorResult; + } +}" +`) + }) + + test('should generate a map of query results with duplicate type names', () => { + const schema: SchemaType = [] + + const queries = [ + { + typeNode: {type: 'unknown'} satisfies TypeNode, + query: '*[_type == "foo"]', + }, + { + typeNode: {type: 'unknown'} satisfies TypeNode, + query: '*[_type == "bar"]', + }, + ] + + const typeGenerator = new TypeGenerator(schema) + typeGenerator.generateTypeNodeTypes('Foo', queries[0].typeNode) + typeGenerator.generateTypeNodeTypes('Foo', queries[1].typeNode) + + const actualOutput = typeGenerator.generateQueryMap(queries) + + expect(actualOutput).toMatchInlineSnapshot(` +"import \\"@sanity/client\\"; +declare module \\"@sanity/client\\" { + interface SanityQueries { + \\"*[_type == \\\\\\"foo\\\\\\"]\\": Foo; + \\"*[_type == \\\\\\"bar\\\\\\"]\\": Foo_2; + } +}" +`) + }) + + test('should generate a map of query results with duplicate query strings', () => { + const schema: SchemaType = [] + + const queries = [ + { + typeNode: {type: 'unknown'} satisfies TypeNode, + query: '*[_type == "foo"]', + }, + { + typeNode: {type: 'unknown'} satisfies TypeNode, + query: '*[_type == "foo"]', + }, + ] + + const typeGenerator = new TypeGenerator(schema) + typeGenerator.generateTypeNodeTypes('Foo', queries[0].typeNode) + typeGenerator.generateTypeNodeTypes('Bar', queries[1].typeNode) + + const actualOutput = typeGenerator.generateQueryMap(queries) + + expect(actualOutput).toMatchInlineSnapshot(` +"import \\"@sanity/client\\"; +declare module \\"@sanity/client\\" { + interface SanityQueries { + \\"*[_type == \\\\\\"foo\\\\\\"]\\": Foo | Bar; + } +}" +`) + }) +}) diff --git a/packages/@sanity/codegen/src/typescript/expressionResolvers.ts b/packages/@sanity/codegen/src/typescript/expressionResolvers.ts index af2a5d7e71e..1ecc25d4765 100644 --- a/packages/@sanity/codegen/src/typescript/expressionResolvers.ts +++ b/packages/@sanity/codegen/src/typescript/expressionResolvers.ts @@ -23,6 +23,7 @@ export interface NamedQueryResult { } const TAGGED_TEMPLATE_ALLOW_LIST = ['groq'] +const FUNCTION_WRAPPER_ALLOW_LIST = ['defineQuery'] /** * resolveExpression takes a node and returns the resolved value of the expression. @@ -125,6 +126,22 @@ export function resolveExpression({ }) } + if ( + babelTypes.isCallExpression(node) && + babelTypes.isIdentifier(node.callee) && + FUNCTION_WRAPPER_ALLOW_LIST.includes(node.callee.name) + ) { + return resolveExpression({ + node: node.arguments[0], + scope, + filename, + file, + resolver, + babelConfig, + params, + }) + } + if (babelTypes.isCallExpression(node)) { return resolveCallExpression({ node, diff --git a/packages/@sanity/codegen/src/typescript/findQueriesInSource.ts b/packages/@sanity/codegen/src/typescript/findQueriesInSource.ts index 089f4318403..1f0a3d7030c 100644 --- a/packages/@sanity/codegen/src/typescript/findQueriesInSource.ts +++ b/packages/@sanity/codegen/src/typescript/findQueriesInSource.ts @@ -1,6 +1,7 @@ import {createRequire} from 'node:module' import {type NodePath, type TransformOptions, traverse} from '@babel/core' +import {type Scope} from '@babel/traverse' import * as babelTypes from '@babel/types' import {getBabelConfig} from '../getBabelConfig' @@ -10,6 +11,8 @@ import {parseSourceFile} from './parseSource' const require = createRequire(__filename) const groqTagName = 'groq' +const defineQueryFunctionName = 'defineQuery' +const groqModuleName = 'groq' const ignoreValue = '@sanity-typegen-ignore' @@ -41,12 +44,17 @@ export function findQueriesInSource( const init = node.init // Look for tagged template expressions that are called with the `groq` tag - if ( + const isGroqTemplateTag = babelTypes.isTaggedTemplateExpression(init) && babelTypes.isIdentifier(init.tag) && - babelTypes.isIdentifier(node.id) && init.tag.name === groqTagName - ) { + + // Look for strings wrapped in a defineQuery function call + const isDefineQueryCall = + babelTypes.isCallExpression(init) && + isImportFrom(groqModuleName, defineQueryFunctionName, scope, init.callee) + + if (babelTypes.isIdentifier(node.id) && (isGroqTemplateTag || isDefineQueryCall)) { // If we find a comment leading the decleration which macthes with ignoreValue we don't add // the query if (declarationLeadingCommentContains(path, ignoreValue)) { @@ -141,3 +149,71 @@ function declarationLeadingCommentContains(path: NodePath, comment: string): boo return false } + +function isImportFrom( + moduleName: string, + importName: string, + scope: Scope, + node: babelTypes.Expression | babelTypes.V8IntrinsicIdentifier, +) { + if (babelTypes.isIdentifier(node)) { + const binding = scope.getBinding(node.name) + if (!binding) { + return false + } + + const {path} = binding + + // import { foo } from 'groq' + if (babelTypes.isImportSpecifier(path.node)) { + return ( + path.node.importKind === 'value' && + path.parentPath && + babelTypes.isImportDeclaration(path.parentPath.node) && + path.parentPath.node.source.value === moduleName && + babelTypes.isIdentifier(path.node.imported) && + path.node.imported.name === importName + ) + } + + // const { defineQuery } = require('groq') + if (babelTypes.isVariableDeclarator(path.node)) { + const {init} = path.node + return ( + babelTypes.isCallExpression(init) && + babelTypes.isIdentifier(init.callee) && + init.callee.name === 'require' && + babelTypes.isStringLiteral(init.arguments[0]) && + init.arguments[0].value === moduleName + ) + } + } + + // import * as foo from 'groq' + // foo.defineQuery(...) + if (babelTypes.isMemberExpression(node)) { + const {object, property} = node + + if (!babelTypes.isIdentifier(object)) { + return false + } + + const binding = scope.getBinding(object.name) + if (!binding) { + return false + } + const {path} = binding + + return ( + babelTypes.isIdentifier(object) && + babelTypes.isIdentifier(property) && + property.name === importName && + babelTypes.isImportNamespaceSpecifier(path.node) && + path.parentPath && + babelTypes.isImportDeclaration(path.parentPath.node) && + path.parentPath.node.source.value === moduleName + ) + } + + return false +} diff --git a/packages/@sanity/codegen/src/typescript/typeGenerator.ts b/packages/@sanity/codegen/src/typescript/typeGenerator.ts index 2a851ed35d9..ed52e972048 100644 --- a/packages/@sanity/codegen/src/typescript/typeGenerator.ts +++ b/packages/@sanity/codegen/src/typescript/typeGenerator.ts @@ -14,6 +14,11 @@ import { const REFERENCE_SYMBOL_NAME = 'internalGroqTypeReferenceTo' const ALL_SCHEMA_TYPES = 'AllSanitySchemaTypes' +type QueryWithTypeNode = { + query: string + typeNode: TypeNode +} + /** * A class used to generate TypeScript types from a given schema * @internal @@ -77,11 +82,8 @@ export class TypeGenerator { generateTypeNodeTypes(identifierName: string, typeNode: TypeNode): string { const type = this.getTypeNodeType(typeNode) - const typeAlias = t.tsTypeAliasDeclaration( - t.identifier(this.getTypeName(identifierName, typeNode)), - null, - type, - ) + const typeName = this.getTypeName(identifierName, typeNode) + const typeAlias = t.tsTypeAliasDeclaration(t.identifier(typeName), null, type) return new CodeGenerator(t.exportNamedDeclaration(typeAlias)).generate().code.trim() } @@ -98,6 +100,57 @@ export class TypeGenerator { return new CodeGenerator(t.exportNamedDeclaration(decleration)).generate().code.trim() } + /** + * Takes a list of queries from the codebase and generates a type declaration + * for SanityClient to consume. + * + * Note: only types that have previously been generated with `generateTypeNodeTypes` + * will be included in the query map. + * + * @param queries - A list of queries to generate a type declaration for + * @returns + * @internal + * @beta + */ + generateQueryMap(queries: QueryWithTypeNode[]): string { + const typesByQuerystring: {[query: string]: string[]} = {} + + for (const query of queries) { + const name = this.typeNameMap.get(query.typeNode) + if (!name) { + continue + } + + typesByQuerystring[query.query] ??= [] + typesByQuerystring[query.query].push(name) + } + + const queryReturnInterface = t.tsInterfaceDeclaration( + t.identifier('SanityQueries'), + null, + [], + t.tsInterfaceBody( + Object.entries(typesByQuerystring).map(([query, types]) => { + return t.tsPropertySignature( + t.stringLiteral(query), + t.tsTypeAnnotation( + t.tsUnionType(types.map((type) => t.tsTypeReference(t.identifier(type)))), + ), + ) + }), + ), + ) + + const declareModule = t.declareModule( + t.stringLiteral('@sanity/client'), + t.blockStatement([queryReturnInterface]), + ) + + const clientImport = t.importDeclaration([], t.stringLiteral('@sanity/client')) + + return new CodeGenerator(t.program([clientImport, declareModule])).generate().code.trim() + } + /** * Since we are sanitizing identifiers we migt end up with collisions. Ie there might be a type mux.video and muxVideo, both these * types would be sanityized into MuxVideo. To avoid this we keep track of the generated type names and add a index to the name. diff --git a/packages/groq/README.md b/packages/groq/README.md index d61975e3aef..1cd25b7eb73 100644 --- a/packages/groq/README.md +++ b/packages/groq/README.md @@ -20,6 +20,20 @@ import groq from 'groq' const query = groq`*[_type == 'products'][0...10]` ``` +## Automatic type inference + +If you are using `@sanity/codegen` you can use `defineQuery` instead of `groq` to +get type inference out of the box: + +```ts +import {defineQuery} from 'groq' + +const query = defineQuery(`*[_type == 'products'][0...10]`) +``` + +In the future we might merge `defineQuery` with `groq`, but this is currently [not +100% supported by TypeScript](https://github.com/microsoft/TypeScript/issues/33304). + ## What is Sanity? What is GROQ? [Sanity](https://www.sanity.io) is a real-time content infrastructure with a scalable, hosted backend featuring a Graph Oriented Query Language (GROQ), asset pipelines and fast edge caches. diff --git a/packages/groq/package.config.ts b/packages/groq/package.config.ts index c43051dd053..79934bacab4 100644 --- a/packages/groq/package.config.ts +++ b/packages/groq/package.config.ts @@ -1,4 +1,11 @@ import baseConfig from '@repo/package.config' import {defineConfig} from '@sanity/pkg-utils' -export default defineConfig(baseConfig) +export default defineConfig({ + ...baseConfig, + legacyExports: false, + bundles: [ + {source: './src/_exports.cts.ts', require: './lib/groq.cjs'}, + {source: './src/_exports.mts.ts', import: './lib/groq.js'}, + ], +}) diff --git a/packages/groq/package.json b/packages/groq/package.json index 32bab9bb7b3..1a51fe4505b 100644 --- a/packages/groq/package.json +++ b/packages/groq/package.json @@ -26,17 +26,16 @@ "license": "MIT", "author": "Sanity.io ", "sideEffects": false, + "type": "module", "exports": { ".": { - "source": "./src/groq.ts", - "import": "./lib/groq.mjs", - "require": "./lib/groq.js", + "require": "./lib/groq.cjs", "default": "./lib/groq.js" }, "./package.json": "./package.json" }, - "main": "./lib/groq.js", - "module": "./lib/groq.esm.js", + "main": "./lib/groq.cjs", + "module": "./lib/groq.js", "types": "./lib/groq.d.ts", "files": [ "lib", diff --git a/packages/groq/src/_exports.cts.ts b/packages/groq/src/_exports.cts.ts new file mode 100644 index 00000000000..8d98d91a252 --- /dev/null +++ b/packages/groq/src/_exports.cts.ts @@ -0,0 +1,11 @@ +import {defineQuery} from './define' +import {groq} from './groq' + +module.exports = groq + +Object.assign(module.exports, {defineQuery}) + +/** + * This is just to fix the typegen for the CJS export, as TS won't pick up on `module.exports` syntax when the package.json has `type: "module"` + */ +export type {groq as default, defineQuery} diff --git a/packages/groq/src/_exports.mts.ts b/packages/groq/src/_exports.mts.ts new file mode 100644 index 00000000000..9867bf049ec --- /dev/null +++ b/packages/groq/src/_exports.mts.ts @@ -0,0 +1,2 @@ +export {defineQuery} from './define' +export {groq as default} from './groq' diff --git a/packages/groq/src/define.ts b/packages/groq/src/define.ts new file mode 100644 index 00000000000..1fdf0f8c35a --- /dev/null +++ b/packages/groq/src/define.ts @@ -0,0 +1,16 @@ +/** + * Define a GROQ query. This is a no-op, but it helps editor integrations + * understand that a string represents a GROQ query in order to provide syntax highlighting + * and other features. + * + * Ideally the `groq` template tag would be used, but we cannot infer types from it until + * microsoft/TypeScript#33304 is resolved. Otherwise, there is no difference between this + * and the `groq` template tag. + * + * @param query - The GROQ query + * @returns The same string as the input + * @public + */ +export function defineQuery(query: Q): Q { + return query +} diff --git a/packages/groq/src/groq.ts b/packages/groq/src/groq.ts index 024c387f1c2..efafab449c1 100644 --- a/packages/groq/src/groq.ts +++ b/packages/groq/src/groq.ts @@ -8,7 +8,7 @@ * @returns The same string as the input * @public */ -export default function groq(strings: TemplateStringsArray, ...keys: any[]): string { +export function groq(strings: TemplateStringsArray, ...keys: any[]): string { const lastIndex = strings.length - 1 return ( strings.slice(0, lastIndex).reduce((acc, str, i) => { diff --git a/packages/groq/test/define.test.cjs b/packages/groq/test/define.test.cjs new file mode 100644 index 00000000000..71d2652b145 --- /dev/null +++ b/packages/groq/test/define.test.cjs @@ -0,0 +1,17 @@ +'use strict' +// Integration test for the Node.js CJS runtime + +const {strict: assert} = require('node:assert') + +const {defineQuery} = require('groq') + +assert.equal(typeof defineQuery, 'function') + +assert.equal(defineQuery(`foo${'bar'}`), `foo${'bar'}`) +assert.equal(defineQuery(`${'bar'}`), `${'bar'}`) +assert.equal(defineQuery(``), ``) +assert.equal(defineQuery(`${'foo'}`), `${'foo'}`) +assert.equal(defineQuery(`${/foo/}bar`), `${/foo/}bar`) +assert.equal(defineQuery(`${'foo'}bar${347}`), `${'foo'}bar${347}`) +assert.equal(defineQuery(`${'foo'}bar${347}${/qux/}`), `${'foo'}bar${347}${/qux/}`) +assert.equal(defineQuery(`${'foo'}${347}qux`), `${'foo'}${347}qux`) diff --git a/packages/groq/test/define.test.mjs b/packages/groq/test/define.test.mjs new file mode 100644 index 00000000000..78f6e5df631 --- /dev/null +++ b/packages/groq/test/define.test.mjs @@ -0,0 +1,16 @@ +// Integration test for the Node.js ESM runtime + +import {strict as assert} from 'node:assert' + +import {defineQuery} from 'groq' + +assert.equal(typeof defineQuery, 'function') + +assert.equal(defineQuery(`foo${'bar'}`), `foo${'bar'}`) +assert.equal(defineQuery(`${'bar'}`), `${'bar'}`) +assert.equal(defineQuery(``), ``) +assert.equal(defineQuery(`${'foo'}`), `${'foo'}`) +assert.equal(defineQuery(`${/foo/}bar`), `${/foo/}bar`) +assert.equal(defineQuery(`${'foo'}bar${347}`), `${'foo'}bar${347}`) +assert.equal(defineQuery(`${'foo'}bar${347}${/qux/}`), `${'foo'}bar${347}${/qux/}`) +assert.equal(defineQuery(`${'foo'}${347}qux`), `${'foo'}${347}qux`)