@sanity-labs/groq-content-mapper 0.0.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +228 -2
- package/dist/bin/contentMapper.d.ts +1 -0
- package/dist/bin/contentMapper.js +11 -0
- package/dist/bin/contentMapper.js.map +1 -0
- package/dist/contentMapper-BMNwbuMF.js +896 -0
- package/dist/contentMapper-BMNwbuMF.js.map +1 -0
- package/dist/index.d.ts +236 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/jest.d.ts +30 -0
- package/dist/jest.d.ts.map +1 -0
- package/dist/jest.js +30 -0
- package/dist/jest.js.map +1 -0
- package/dist/next.d.ts +46 -0
- package/dist/next.d.ts.map +1 -0
- package/dist/next.js +122 -0
- package/dist/next.js.map +1 -0
- package/dist/nodeHooks-CRMKGlwf.js +21 -0
- package/dist/nodeHooks-CRMKGlwf.js.map +1 -0
- package/dist/nodeHooks.d.ts +8 -0
- package/dist/nodeHooks.d.ts.map +1 -0
- package/dist/nodeHooks.js +1 -0
- package/dist/register.d.ts +1 -0
- package/dist/register.js +6 -0
- package/dist/register.js.map +1 -0
- package/dist/runtime-DNC5x51t.js +23 -0
- package/dist/runtime-DNC5x51t.js.map +1 -0
- package/dist/vite.d.ts +32 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +34 -0
- package/dist/vite.js.map +1 -0
- package/package.json +73 -4
package/README.md
CHANGED
|
@@ -1,2 +1,228 @@
|
|
|
1
|
-
# groq-content-mapper
|
|
2
|
-
|
|
1
|
+
# @sanity-labs/groq-content-mapper
|
|
2
|
+
|
|
3
|
+
Typed `.groq` file imports for TypeScript 7.1 and later. A query kept in its own file is imported as a string that carries its result and params types, inferred from your Sanity schema.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- **TypeScript 7.1 or later.** Content mappers do not exist in earlier versions. 7.1 is currently a beta: `npm install -D typescript@beta`.
|
|
8
|
+
- **Node.js 22.12 or later.** On Node 26.5 and later, run `.groq` imports with `--experimental-import-text`, or with the preload below. On earlier Node, run `node --import @sanity-labs/groq-content-mapper/register`.
|
|
9
|
+
- **Type-check with `tsc --runExternalCode`.** Without that flag the mapper does not run, and `.groq` imports are not typed.
|
|
10
|
+
|
|
11
|
+
```jsonc
|
|
12
|
+
// package.json
|
|
13
|
+
{
|
|
14
|
+
"scripts": {
|
|
15
|
+
"typecheck": "tsc --noEmit --runExternalCode",
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```groq
|
|
23
|
+
// postsBySearch.groq
|
|
24
|
+
*[_type == "post" && title match $searchQuery] | order(_createdAt desc) [$start...$end] {
|
|
25
|
+
_id,
|
|
26
|
+
title,
|
|
27
|
+
slug
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import postsBySearch, {type QueryResponse} from './postsBySearch.groq' with {type: 'text'}
|
|
33
|
+
|
|
34
|
+
const posts = await client.fetch(postsBySearch, {searchQuery: 'foo', start: 0, end: 10})
|
|
35
|
+
// ^? QueryResponse, with the params checked
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Types come from the file as it is. Nothing is written to disk, and nothing is added to the global scope.
|
|
39
|
+
|
|
40
|
+
## What an import gives you
|
|
41
|
+
|
|
42
|
+
Each `.groq` file has three exports:
|
|
43
|
+
|
|
44
|
+
- The default export is the query string, typed as its own text, and still assignable to `string`. The type includes the file's trailing newline, because that is what the import evaluates to.
|
|
45
|
+
- `QueryResponse` is the result type. `[0]` is `T | null`.
|
|
46
|
+
- `QueryParams` is the object the `$` placeholders expect. A placeholder compared with a field takes the field's type, `match` takes a string, slice bounds are numbers, and anything the schema says nothing about is `unknown`. A query without placeholders has `QueryParams = Record<string, never>`.
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
export type QueryResponse = Array<{_id: string; title: string | null; slug: Slug | null}>
|
|
50
|
+
export type QueryParams = {searchQuery: string; start: number; end: number}
|
|
51
|
+
declare const query: '*[_type == "post" && title match $searchQuery] …\n' & {
|
|
52
|
+
readonly __groqResult?: QueryResponse
|
|
53
|
+
readonly __groqParams?: QueryParams
|
|
54
|
+
}
|
|
55
|
+
export default query
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`client.fetch(query, params)` infers both types once `@sanity/client` includes [sanity-io/client#1373](https://github.com/sanity-io/client/pull/1373). With an older client, pass the type yourself: `client.fetch<QueryResponse>(query, params)`. `GroqQuery<Query, Result, Params>`, `GroqResult<Q>` and `GroqParams<Q>` are exported for the same shape.
|
|
59
|
+
|
|
60
|
+
### Imports
|
|
61
|
+
|
|
62
|
+
A value import needs `with {type: 'text'}`, the [standard text import](https://github.com/tc39/proposal-import-text). At run time the default export is the file's content.
|
|
63
|
+
|
|
64
|
+
A type-only import needs no attribute. It is erased, so the query string stays out of the bundle:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import type {QueryResponse} from './postsBySearch.groq'
|
|
68
|
+
|
|
69
|
+
export function PostList(props: {posts: QueryResponse}) {}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
When a file needs both the query and its types, write one statement:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import postsBySearch, {type QueryResponse} from './postsBySearch.groq' with {type: 'text'}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
A separate `import type` from the same file gets merged by ESLint's `import-x/no-duplicates` autofix, which drops or mangles the attribute.
|
|
79
|
+
|
|
80
|
+
## Setup
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
npm install -D @sanity-labs/groq-content-mapper typescript@beta
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
If the app runs `.groq` imports on Node through the preload (rather than as a bundle), install the package as a regular dependency instead.
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
npx sanity schema extract
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`schema` is required, and the tsconfig is the only configuration. Paths are relative to it:
|
|
93
|
+
|
|
94
|
+
```jsonc
|
|
95
|
+
// tsconfig.json
|
|
96
|
+
{
|
|
97
|
+
"compilerOptions": {/* ... */},
|
|
98
|
+
"contentMappers": [
|
|
99
|
+
{
|
|
100
|
+
"package": "@sanity-labs/groq-content-mapper",
|
|
101
|
+
"extensions": [".groq"],
|
|
102
|
+
"options": {"schema": "./schema.json"},
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Type-check with the `tsc --noEmit --runExternalCode` script above. The schema file is watched, so extracting it again retypes the imports. A query that does not parse, or a missing schema, still resolves: the unknown parts are `unknown`, and the diagnostic says why. A GROQ syntax error is reported on the `.groq` file as `error groq2`.
|
|
109
|
+
|
|
110
|
+
### Editor
|
|
111
|
+
|
|
112
|
+
In VS Code or Cursor, install the TypeScript 7 extension (`TypeScriptTeam.native-preview`) and point it at the project's TypeScript. The extension bundles 7.0:
|
|
113
|
+
|
|
114
|
+
```jsonc
|
|
115
|
+
// .vscode/settings.json
|
|
116
|
+
{
|
|
117
|
+
"js/ts.experimental.useTsgo": true,
|
|
118
|
+
"js/ts.tsdk.path": "./node_modules/typescript",
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Trust the workspace, and allow the extension to use the workspace's TypeScript when it asks.
|
|
123
|
+
|
|
124
|
+
## One schema per folder
|
|
125
|
+
|
|
126
|
+
A codebase that queries more than one project or dataset has one schema per folder. TypeScript allows one mapper entry per extension, so list the folders inside that entry with `schemas`:
|
|
127
|
+
|
|
128
|
+
```jsonc
|
|
129
|
+
{
|
|
130
|
+
"contentMappers": [
|
|
131
|
+
{
|
|
132
|
+
"package": "@sanity-labs/groq-content-mapper",
|
|
133
|
+
"extensions": [".groq"],
|
|
134
|
+
"options": {
|
|
135
|
+
"schemas": [
|
|
136
|
+
{"include": "src/queries/blog", "schema": "schemas/blog.json"},
|
|
137
|
+
{"include": ["src/queries/shop", "src/queries/checkout"], "schema": "schemas/shop.json"},
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
A file uses the schema of the deepest folder that contains it, and `schema` when no folder does. Without a `schema`, a file outside every folder gets a diagnostic.
|
|
146
|
+
|
|
147
|
+
A `package.json` `imports` alias resolves to the file, so the folder's schema still applies. Node, Bun, Deno and the bundlers resolve `imports` themselves:
|
|
148
|
+
|
|
149
|
+
```jsonc
|
|
150
|
+
// package.json
|
|
151
|
+
{
|
|
152
|
+
"imports": {
|
|
153
|
+
"#queries/*": "./src/queries/*",
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
import posts from '#queries/blog/posts.groq' with {type: 'text'}
|
|
160
|
+
import products from '#queries/shop/products.groq' with {type: 'text'}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Running the imports
|
|
164
|
+
|
|
165
|
+
`with {type: 'text'}` makes the runtime or bundler deliver the file as a string. Where one does not, one line fills in.
|
|
166
|
+
|
|
167
|
+
| Runtime or bundler | Setup |
|
|
168
|
+
| --------------------------------------------- | ----------------------------------------------------------------------- |
|
|
169
|
+
| Bun, Deno, esbuild, `Bun.build` | nothing |
|
|
170
|
+
| webpack 5.102 and later, Rspack 1.7 and later | nothing |
|
|
171
|
+
| webpack before 5.102 | `{test: /\.groq$/, type: 'asset/source'}` in `module.rules` |
|
|
172
|
+
| Rolldown | `moduleTypes: {'.groq': 'text'}` |
|
|
173
|
+
| Node 26.5 and later | `--experimental-import-text`, or the preload below |
|
|
174
|
+
| Node 22.12 and later | `node --import @sanity-labs/groq-content-mapper/register main.ts` |
|
|
175
|
+
| Vite, Vitest, Rollup | the Vite plugin |
|
|
176
|
+
| Sanity Studio, App SDK apps | the Vite plugin, in `vite: {plugins: [groqFiles()]}` of `sanity.cli.ts` |
|
|
177
|
+
| Next.js 16.3 and later | `withGroqFiles` |
|
|
178
|
+
| Jest | the Jest transformer, with tests compiled to CommonJS |
|
|
179
|
+
|
|
180
|
+
### Node
|
|
181
|
+
|
|
182
|
+
```sh
|
|
183
|
+
node --import @sanity-labs/groq-content-mapper/register main.ts
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Vite, Vitest, Rollup
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
// vite.config.ts
|
|
190
|
+
import groqFiles from '@sanity-labs/groq-content-mapper/vite'
|
|
191
|
+
import {defineConfig} from 'vite'
|
|
192
|
+
|
|
193
|
+
export default defineConfig({plugins: [groqFiles()]})
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Vite's dev server does not handle the attribute, so the plugin is needed there even when a production build would work without it.
|
|
197
|
+
|
|
198
|
+
### Next.js
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
// next.config.ts
|
|
202
|
+
import {withGroqFiles} from '@sanity-labs/groq-content-mapper/next'
|
|
203
|
+
|
|
204
|
+
export default withGroqFiles(nextConfig)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`withGroqFiles` covers both Turbopack and the webpack Next bundles. It also replaces the type check `next build` runs, which does not pass `--runExternalCode` and so fails on every `.groq` import. The same check runs with the flag after the compile. If the config already sets `typescript.ignoreBuildErrors`, no check is run.
|
|
208
|
+
|
|
209
|
+
### Jest
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
// jest.config.js
|
|
213
|
+
export default {
|
|
214
|
+
transform: {'\\.groq$': '@sanity-labs/groq-content-mapper/jest'},
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Jest rejects `type: 'text'` in its ES module loader, so this is for tests compiled to CommonJS, where the attribute is dropped.
|
|
219
|
+
|
|
220
|
+
## Limitations
|
|
221
|
+
|
|
222
|
+
- A comparison against `^` types its placeholder as `unknown`.
|
|
223
|
+
- `score()` is not typed yet, so `_score` is `null`, and a `[_score > 0]` filter empties the result type.
|
|
224
|
+
- `include` takes folders, not globs.
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { t as ContentMapper } from "../contentMapper-BMNwbuMF.js";
|
|
2
|
+
try {
|
|
3
|
+
await new ContentMapper().serve({
|
|
4
|
+
input: process.stdin,
|
|
5
|
+
output: process.stdout
|
|
6
|
+
});
|
|
7
|
+
} catch (error) {
|
|
8
|
+
process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`), process.exitCode = 1;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
//# sourceMappingURL=contentMapper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contentMapper.js","names":[],"sources":["../../src/bin/contentMapper.ts"],"sourcesContent":["// The command in package.json `typescript.contentMapper.exec`. TypeScript 7.1 starts it with\n// stdio pipes, speaks the content mapper protocol over them, and closes stdin to stop it.\nimport {ContentMapper} from '../contentMapper.js'\n\ntry {\n await new ContentMapper().serve({input: process.stdin, output: process.stdout})\n} catch (error) {\n process.stderr.write(\n `${error instanceof Error ? (error.stack ?? error.message) : String(error)}\\n`,\n )\n process.exitCode = 1\n}\n"],"mappings":";AAIA,IAAI;CACF,MAAM,IAAI,cAAc,CAAC,CAAC,MAAM;EAAC,OAAO,QAAQ;EAAO,QAAQ,QAAQ;CAAM,CAAC;AAChF,SAAS,OAAO;CAId,AAHA,QAAQ,OAAO,MACb,GAAG,iBAAiB,QAAS,MAAM,SAAS,MAAM,UAAW,OAAO,KAAK,EAAE,GAC7E,GACA,QAAQ,WAAW;AACrB"}
|