@sanity-labs/groq-content-mapper 0.1.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.
Files changed (2) hide show
  1. package/README.md +40 -41
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -1,6 +1,23 @@
1
1
  # @sanity-labs/groq-content-mapper
2
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:
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
4
21
 
5
22
  ```groq
6
23
  // postsBySearch.groq
@@ -18,18 +35,17 @@ const posts = await client.fetch(postsBySearch, {searchQuery: 'foo', start: 0, e
18
35
  // ^? QueryResponse, with the params checked
19
36
  ```
20
37
 
21
- This is experimental. It is a TypeScript 7.1 [content mapper](https://devblogs.microsoft.com/typescript/announcing-typescript-7-1-beta/): there is no generated declaration file, and nothing for earlier TypeScript releases. The type generation itself comes from [`@sanity/codegen`](https://github.com/sanity-io/codegen), which this package depends on, so you do not install it yourself.
38
+ Types come from the file as it is. Nothing is written to disk, and nothing is added to the global scope.
22
39
 
23
40
  ## What an import gives you
24
41
 
25
- Each `.groq` file is a module with three exports:
42
+ Each `.groq` file has three exports:
26
43
 
27
- - The default export is the query string, typed as its own literal with the result and params types attached as optional phantom properties. It is still assignable to `string`. The literal is the file's exact content, trailing newline included, because that is what a text import evaluates to.
28
- - `QueryResponse` is the result type, evaluated the way `defineQuery` results are, so `[0]` is `T | null`.
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`.
29
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>`.
30
47
 
31
48
  ```ts
32
- // What './postsBySearch.groq' resolves to
33
49
  export type QueryResponse = Array<{_id: string; title: string | null; slug: Slug | null}>
34
50
  export type QueryParams = {searchQuery: string; start: number; end: number}
35
51
  declare const query: '*[_type == "post" && title match $searchQuery] …\n' & {
@@ -39,13 +55,13 @@ declare const query: '*[_type == "post" && title match $searchQuery] …\n' & {
39
55
  export default query
40
56
  ```
41
57
 
42
- Nothing is declared globally and nothing is imported. `@sanity/client` reads the brand structurally once [sanity-io/client#1373](https://github.com/sanity-io/client/pull/1373) is released, so `client.fetch(query, params)` infers both types. With an older client, pass the type yourself: `client.fetch<QueryResponse>(query, params)`. The brand is also exported as `GroqQuery<Query, Result, Params>`, with `GroqResult<Q>` and `GroqParams<Q>` to read it back.
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.
43
59
 
44
60
  ### Imports
45
61
 
46
- A value import needs the `with {type: 'text'}` attribute, which is the standard text import ([TC39 import text](https://github.com/tc39/proposal-import-text)): at run time the default export is the file's content. TypeScript ignores the attribute and asks the content mapper for the type.
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.
47
63
 
48
- A type-only import needs no attribute, and since it is erased it keeps the query string out of the bundle. Use it in code that only needs the types, such as components rendering a result:
64
+ A type-only import needs no attribute. It is erased, so the query string stays out of the bundle:
49
65
 
50
66
  ```ts
51
67
  import type {QueryResponse} from './postsBySearch.groq'
@@ -53,31 +69,27 @@ import type {QueryResponse} from './postsBySearch.groq'
53
69
  export function PostList(props: {posts: QueryResponse}) {}
54
70
  ```
55
71
 
56
- When a file needs both the query and its types, write one statement with inline type specifiers:
72
+ When a file needs both the query and its types, write one statement:
57
73
 
58
74
  ```ts
59
75
  import postsBySearch, {type QueryResponse} from './postsBySearch.groq' with {type: 'text'}
60
76
  ```
61
77
 
62
- rather than a value import plus a separate `import type` from the same file. ESLint's `import-x/no-duplicates` autofix merges the two and drops or mangles the attribute in the process.
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.
63
79
 
64
80
  ## Setup
65
81
 
66
- You need TypeScript 7.1 or later and Node 22.12 or later.
67
-
68
82
  ```sh
69
83
  npm install -D @sanity-labs/groq-content-mapper typescript@beta
70
84
  ```
71
85
 
72
- If your app runs `.groq` imports on Node through the `register` preload below (rather than as a bundle), install the package as a regular dependency instead.
73
-
74
- Write the schema file with the Sanity CLI:
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.
75
87
 
76
88
  ```sh
77
89
  npx sanity schema extract
78
90
  ```
79
91
 
80
- Register the mapper in `tsconfig.json`, next to `compilerOptions`. The tsconfig is the only place it is configured, and `schema` is required:
92
+ `schema` is required, and the tsconfig is the only configuration. Paths are relative to it:
81
93
 
82
94
  ```jsonc
83
95
  // tsconfig.json
@@ -93,22 +105,11 @@ Register the mapper in `tsconfig.json`, next to `compilerOptions`. The tsconfig
93
105
  }
94
106
  ```
95
107
 
96
- Paths are relative to the tsconfig. Type-check with `--runExternalCode`, the flag that lets `tsc` start content mappers:
97
-
98
- ```jsonc
99
- // package.json
100
- {
101
- "scripts": {
102
- "typecheck": "tsc --noEmit --runExternalCode",
103
- },
104
- }
105
- ```
106
-
107
- The schema file is watched, so extracting it again retypes the imports. A file that cannot be typed, because the query does not parse or the schema is missing, still resolves, with `unknown` for what could not be inferred and a diagnostic saying why. GROQ syntax errors are reported at the `.groq` file as `error groq2`.
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`.
108
109
 
109
110
  ### Editor
110
111
 
111
- In VS Code or Cursor, install the TypeScript 7 extension (`TypeScriptTeam.native-preview`) and point it at the project's TypeScript, since the extension bundles 7.0:
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:
112
113
 
113
114
  ```jsonc
114
115
  // .vscode/settings.json
@@ -122,7 +123,7 @@ Trust the workspace, and allow the extension to use the workspace's TypeScript w
122
123
 
123
124
  ## One schema per folder
124
125
 
125
- A codebase that queries more than one project or dataset has one schema per folder. TypeScript registers a content mapper by file extension only, and rejects two entries for `.groq`, so the folders are listed inside the one entry with `schemas`:
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`:
126
127
 
127
128
  ```jsonc
128
129
  {
@@ -141,9 +142,9 @@ A codebase that queries more than one project or dataset has one schema per fold
141
142
  }
142
143
  ```
143
144
 
144
- A file is typed against the schema of the deepest folder that contains it, and against `schema` when no folder does. Without a `schema`, a file outside every folder gets a diagnostic.
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.
145
146
 
146
- Folders work well with a `package.json` `imports` alias. TypeScript resolves the alias to the file before asking the mapper, so each folder's schema still applies, and Node, Bun, Deno and the bundlers resolve `imports` themselves:
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:
147
148
 
148
149
  ```jsonc
149
150
  // package.json
@@ -161,7 +162,7 @@ import products from '#queries/shop/products.groq' with {type: 'text'}
161
162
 
162
163
  ## Running the imports
163
164
 
164
- At run time `with {type: 'text'}` makes the runtime or bundler deliver the file as a string. Many do this on their own; for the rest, this package has one entry each. None of them runs type generation.
165
+ `with {type: 'text'}` makes the runtime or bundler deliver the file as a string. Where one does not, one line fills in.
165
166
 
166
167
  | Runtime or bundler | Setup |
167
168
  | --------------------------------------------- | ----------------------------------------------------------------------- |
@@ -182,8 +183,6 @@ At run time `with {type: 'text'}` makes the runtime or bundler deliver the file
182
183
  node --import @sanity-labs/groq-content-mapper/register main.ts
183
184
  ```
184
185
 
185
- The preload uses `module.registerHooks` where Node has it (22.15 and later) and `module.register` before that.
186
-
187
186
  ### Vite, Vitest, Rollup
188
187
 
189
188
  ```ts
@@ -194,7 +193,7 @@ import {defineConfig} from 'vite'
194
193
  export default defineConfig({plugins: [groqFiles()]})
195
194
  ```
196
195
 
197
- Vite's dev server does not handle the attribute itself, so the plugin is needed there even where the build would work without it.
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.
198
197
 
199
198
  ### Next.js
200
199
 
@@ -205,7 +204,7 @@ import {withGroqFiles} from '@sanity-labs/groq-content-mapper/next'
205
204
  export default withGroqFiles(nextConfig)
206
205
  ```
207
206
 
208
- Turbopack handles the attribute on its own. The wrapper adds an `asset/source` rule for the webpack that Next bundles, which predates webpack's built-in support. It also takes over the type check `next build` runs: Next runs `tsc` without `--runExternalCode`, so its check fails on every `.groq` import. The wrapper turns Next's check off and runs the same check with the flag after the compile. If your config already sets `typescript.ignoreBuildErrors`, no check is run.
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.
209
208
 
210
209
  ### Jest
211
210
 
@@ -216,12 +215,12 @@ export default {
216
215
  }
217
216
  ```
218
217
 
219
- Jest's ES module loader rejects `type: 'text'`, so this works for tests compiled to CommonJS, where Babel drops the attribute.
218
+ Jest rejects `type: 'text'` in its ES module loader, so this is for tests compiled to CommonJS, where the attribute is dropped.
220
219
 
221
220
  ## Limitations
222
221
 
223
- - A comparison against `^` (the parent scope) types its placeholder as `unknown`.
224
- - groq-js does not model `score()` yet, so `_score` is typed as `null`, and a `[_score > 0]` filter empties the result type.
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.
225
224
  - `include` takes folders, not globs.
226
225
 
227
226
  ## License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity-labs/groq-content-mapper",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Typed .groq file imports for TypeScript 7.1, from your Sanity schema",
5
5
  "keywords": [
6
6
  "content-mapper",
@@ -42,7 +42,7 @@
42
42
  "dependencies": {
43
43
  "@babel/generator": "^7.29.7",
44
44
  "@babel/types": "^7.29.7",
45
- "@sanity/codegen": "^8.2.0",
45
+ "@sanity/codegen": "8.2.0",
46
46
  "groq-js": "^2.0.0",
47
47
  "valibot": "^1.4.2"
48
48
  },