@sanity-labs/groq-wasm
WASM bindings for GROQ linting and formatting.
This package provides TypeScript wrappers around:
- groq-lint (Rust) - GROQ query linting
- groq-format (Rust) - GROQ query formatting
Both are compiled to WebAssembly for use in Node.js and browsers without requiring the Rust toolchain.
Installation
npm install @sanity-labs/groq-wasm
Usage
import { lint, format, validate } from '@sanity-labs/groq-wasm'
// WASM loads when the module is imported. lint, format, and validate are sync.
// Lint a query
const findings = lint('*[_type == "post"]{ author-> }')
console.log(findings)
// [{ ruleId: 'join-in-filter', message: '...', severity: 'error' }]
// Format a query
const formatted = format('*[_type=="post"]{title,body}')
console.log(formatted)
// '*[_type == "post"]{ title, body }'
// Validate syntax without linting or formatting
const result = validate('*["é" == ]')
if (!result.ok) {
const { message, start, end } = result.error
// start/end are UTF-16 indices: query.slice(start, end) === ']'
console.log(message, start, end)
}
API
Importing the package loads the WASM module. lint(), format(), and validate() are synchronous after that import resolves.
Linting
lint(query: string, config?: WasmLintConfig): Finding[]
Lint a GROQ query and return findings.
interface WasmLintConfig {
rules?: Record<string, boolean> // Enable/disable specific rules
}
Formatting
format(query: string, config?: WasmFormatConfig): string
Format a GROQ query.
interface WasmFormatConfig {
width?: number // Max line width (default: 80)
}
isValidSyntax(query: string): boolean
Check if a query has valid GROQ syntax by attempting to format it. An empty query is treated as valid. For a structured parse error, use validate().
Validation
validate(query: string): ValidateResult
Parse a GROQ query with groq-parser and return a result value. Syntax errors are not thrown.
type ValidateResult = { ok: true } | { ok: false; error: ValidateError }
interface ValidateError {
message: string
/** Inclusive 0-based index into the JavaScript string (UTF-16 code units). */
start: number
/** Exclusive 0-based index into the JavaScript string (UTF-16 code units). */
end: number
/** 1-based line of `start`. */
line: number
/** 1-based column of `start`, counted in UTF-16 code units. */
column: number
/** 1-based line of `end`. */
endLine: number
/** 1-based column of `end`, counted in UTF-16 code units. */
endColumn: number
}
groq-parser reports UTF-8 byte offsets (ParseError positions). validate converts start and end to UTF-16 code unit indices so they match JavaScript string indexing (query.slice(start, end)) and editors such as Sanity Vision. Lines and columns are 1-based, and columns count UTF-16 code units, the same convention as line / column on lint finding locations.
start === end means the parser reported a caret on a boundary rather than a token span. An empty or whitespace-only query fails with message "no query" at offset 0. That differs from lint('') (no findings), format('') (returns the input), and isValidSyntax('') (returns true).
$parameter references are syntactically valid. validate does not check that parameters were declared.
lint and format are unchanged: invalid syntax still throws WasmError with code PARSE_ERROR.
Constants
DEFAULT_WIDTH: number
Default line width for formatting (80).
RULE_ID_MAP: Record<string, string>
Mapping from Rust rule IDs (snake_case) to TS convention (kebab-case).
Parameters
extractParams(query: string): QueryParam[]
List the $parameters a query references, using groq-parser. Returns one entry per distinct name (without the $) in order of first reference, each with every reference's span.
import { extractParams } from '@sanity-labs/groq-wasm'
const query = '*[_type == $type][0...$limit]{ "same": $type }'
const params = extractParams(query)
params.map((param) => param.name)
// ['type', 'limit']
params[0].references.map(({ start, end }) => query.slice(start, end))
// ['$type', '$type']
interface QueryParam {
name: string // Without the `@sanity-labs/groq-wasm
WASM bindings for GROQ linting and formatting.
This package provides TypeScript wrappers around:
- groq-lint (Rust) - GROQ query linting
- groq-format (Rust) - GROQ query formatting
Both are compiled to WebAssembly for use in Node.js and browsers without requiring the Rust toolchain.
Installation
npm install @sanity-labs/groq-wasm
Usage
import { lint, format, validate } from '@sanity-labs/groq-wasm'
// WASM loads when the module is imported. lint, format, and validate are sync.
// Lint a query
const findings = lint('*[_type == "post"]{ author-> }')
console.log(findings)
// [{ ruleId: 'join-in-filter', message: '...', severity: 'error' }]
// Format a query
const formatted = format('*[_type=="post"]{title,body}')
console.log(formatted)
// '*[_type == "post"]{ title, body }'
// Validate syntax without linting or formatting
const result = validate('*["é" == ]')
if (!result.ok) {
const { message, start, end } = result.error
// start/end are UTF-16 indices: query.slice(start, end) === ']'
console.log(message, start, end)
}
API
Importing the package loads the WASM module. __INLINE_CODE_0__, __INLINE_CODE_1__, and __INLINE_CODE_2__ are synchronous after that import resolves.
Linting
__INLINE_CODE_3__
Lint a GROQ query and return findings.
interface WasmLintConfig {
rules?: Record<string, boolean> // Enable/disable specific rules
}
Formatting
__INLINE_CODE_4__
Format a GROQ query.
interface WasmFormatConfig {
width?: number // Max line width (default: 80)
}
__INLINE_CODE_5__
Check if a query has valid GROQ syntax by attempting to format it. An empty query is treated as valid. For a structured parse error, use __INLINE_CODE_6__.
Validation
__INLINE_CODE_7__
Parse a GROQ query with groq-parser and return a result value. Syntax errors are not thrown.
type ValidateResult = { ok: true } | { ok: false; error: ValidateError }
interface ValidateError {
message: string
/** Inclusive 0-based index into the JavaScript string (UTF-16 code units). */
start: number
/** Exclusive 0-based index into the JavaScript string (UTF-16 code units). */
end: number
/** 1-based line of `start`. */
line: number
/** 1-based column of `start`, counted in UTF-16 code units. */
column: number
/** 1-based line of `end`. */
endLine: number
/** 1-based column of `end`, counted in UTF-16 code units. */
endColumn: number
}
groq-parser reports UTF-8 byte offsets (__INLINE_CODE_8__ positions). __INLINE_CODE_9__ converts __INLINE_CODE_10__ and __INLINE_CODE_11__ to UTF-16 code unit indices so they match JavaScript string indexing (__INLINE_CODE_12__) and editors such as Sanity Vision. Lines and columns are 1-based, and columns count UTF-16 code units, the same convention as __INLINE_CODE_13__ / __INLINE_CODE_14__ on lint finding locations.
__INLINE_CODE_15__ means the parser reported a caret on a boundary rather than a token span. An empty or whitespace-only query fails with message __INLINE_CODE_16__ at offset 0. That differs from __INLINE_CODE_17__ (no findings), __INLINE_CODE_18__ (returns the input), and __INLINE_CODE_19__ (returns __INLINE_CODE_20__).
__INLINE_CODE_21__ references are syntactically valid. __INLINE_CODE_22__ does not check that parameters were declared.
__INLINE_CODE_23__ and __INLINE_CODE_24__ are unchanged: invalid syntax still throws __INLINE_CODE_25__ with code __INLINE_CODE_26__.
Constants
__INLINE_CODE_27__
Default line width for formatting (80).
__INLINE_CODE_28__
Mapping from Rust rule IDs (snake_case) to TS convention (kebab-case).
Parameters
__INLINE_CODE_29__
List the __INLINE_CODE_30__ a query references, using groq-parser. Returns one entry per distinct name (without the __INLINE_CODE_31__) in order of first reference, each with every reference's span.
import { extractParams } from '@sanity-labs/groq-wasm'
const query = '*[_type == $type][0...$limit]{ "same": $type }'
const params = extractParams(query)
params.map((param) => param.name)
// ['type', 'limit']
params[0].references.map(({ start, end }) => query.slice(start, end))
// ['$type', '$type']
references: ParamReference[] // Source order, never empty
}
interface ParamReference {
start: number // Inclusive index of the `@sanity-labs/groq-wasm
WASM bindings for GROQ linting and formatting.
This package provides TypeScript wrappers around:
- groq-lint (Rust) - GROQ query linting
- groq-format (Rust) - GROQ query formatting
Both are compiled to WebAssembly for use in Node.js and browsers without requiring the Rust toolchain.
Installation
npm install @sanity-labs/groq-wasm
Usage
import { lint, format, validate } from '@sanity-labs/groq-wasm'
// WASM loads when the module is imported. lint, format, and validate are sync.
// Lint a query
const findings = lint('*[_type == "post"]{ author-> }')
console.log(findings)
// [{ ruleId: 'join-in-filter', message: '...', severity: 'error' }]
// Format a query
const formatted = format('*[_type=="post"]{title,body}')
console.log(formatted)
// '*[_type == "post"]{ title, body }'
// Validate syntax without linting or formatting
const result = validate('*["é" == ]')
if (!result.ok) {
const { message, start, end } = result.error
// start/end are UTF-16 indices: query.slice(start, end) === ']'
console.log(message, start, end)
}
API
Importing the package loads the WASM module. __INLINE_CODE_0__, __INLINE_CODE_1__, and __INLINE_CODE_2__ are synchronous after that import resolves.
Linting
__INLINE_CODE_3__
Lint a GROQ query and return findings.
interface WasmLintConfig {
rules?: Record<string, boolean> // Enable/disable specific rules
}
Formatting
__INLINE_CODE_4__
Format a GROQ query.
interface WasmFormatConfig {
width?: number // Max line width (default: 80)
}
__INLINE_CODE_5__
Check if a query has valid GROQ syntax by attempting to format it. An empty query is treated as valid. For a structured parse error, use __INLINE_CODE_6__.
Validation
__INLINE_CODE_7__
Parse a GROQ query with groq-parser and return a result value. Syntax errors are not thrown.
type ValidateResult = { ok: true } | { ok: false; error: ValidateError }
interface ValidateError {
message: string
/** Inclusive 0-based index into the JavaScript string (UTF-16 code units). */
start: number
/** Exclusive 0-based index into the JavaScript string (UTF-16 code units). */
end: number
/** 1-based line of `start`. */
line: number
/** 1-based column of `start`, counted in UTF-16 code units. */
column: number
/** 1-based line of `end`. */
endLine: number
/** 1-based column of `end`, counted in UTF-16 code units. */
endColumn: number
}
groq-parser reports UTF-8 byte offsets (__INLINE_CODE_8__ positions). __INLINE_CODE_9__ converts __INLINE_CODE_10__ and __INLINE_CODE_11__ to UTF-16 code unit indices so they match JavaScript string indexing (__INLINE_CODE_12__) and editors such as Sanity Vision. Lines and columns are 1-based, and columns count UTF-16 code units, the same convention as __INLINE_CODE_13__ / __INLINE_CODE_14__ on lint finding locations.
__INLINE_CODE_15__ means the parser reported a caret on a boundary rather than a token span. An empty or whitespace-only query fails with message __INLINE_CODE_16__ at offset 0. That differs from __INLINE_CODE_17__ (no findings), __INLINE_CODE_18__ (returns the input), and __INLINE_CODE_19__ (returns __INLINE_CODE_20__).
__INLINE_CODE_21__ references are syntactically valid. __INLINE_CODE_22__ does not check that parameters were declared.
__INLINE_CODE_23__ and __INLINE_CODE_24__ are unchanged: invalid syntax still throws __INLINE_CODE_25__ with code __INLINE_CODE_26__.
Constants
__INLINE_CODE_27__
Default line width for formatting (80).
__INLINE_CODE_28__
Mapping from Rust rule IDs (snake_case) to TS convention (kebab-case).
Parameters
__INLINE_CODE_29__
List the __INLINE_CODE_30__ a query references, using groq-parser. Returns one entry per distinct name (without the __INLINE_CODE_31__) in order of first reference, each with every reference's span.
import { extractParams } from '@sanity-labs/groq-wasm'
const query = '*[_type == $type][0...$limit]{ "same": $type }'
const params = extractParams(query)
params.map((param) => param.name)
// ['type', 'limit']
params[0].references.map(({ start, end }) => query.slice(start, end))
// ['$type', '$type']
(UTF-16 code units)
end: number // Exclusive index (UTF-16 code units)
line: number // 1-based line of `start`
column: number // 1-based column of `start` (UTF-16 code units)
endLine: number
endColumn: number
}
- Parameters are found in filters, projections, slices, element indexes, function arguments and nested subqueries.
$nametext inside strings and comments is not a reference.- Parameters bound by a custom function (
fn ns::f($p) = ...) are not reported from inside that function's body. - An empty or whitespace-only query returns
[]. Invalid syntax throwsWasmErrorwith codePARSE_ERROR, likelint()andformat().
Lint Rules
The following rules are available (from Rust groq-lint):
| Rule ID | Severity | Description |
|---|---|---|
join-in-filter |
error | Dereference (->) inside filter prevents optimization |
join-to-get-id |
warning | Using -> to get _id (use ._ref instead) |
computed-value-in-filter |
error | Computed values in filter prevent optimization |
match-on-id |
info | Using match on _id with wildcard |
order-on-expr |
error | Ordering on computed values |
deep-pagination |
warning | Large offset in slice (>=1000) |
large-pages |
warning | Fetching >100 results from index 0 |
non-literal-comparison |
error | Comparing two non-literal expressions |
count-in-correlated-subquery |
info | count() on correlated subqueries |
very-large-query |
error | Query exceeds 10KB |
extremely-large-query |
error | Query exceeds 100KB |
many-joins |
warning | Query has >10 dereference operators |
unlimited-query |
warning | Top-level query without a slice or element limit |
Error Handling
import { WasmError } from '@sanity-labs/groq-wasm'
try {
const findings = lint(query)
} catch (error) {
if (error instanceof WasmError) {
switch (error.code) {
case 'NOT_INITIALIZED':
// Call initWasm() first
break
case 'PARSE_ERROR':
// Invalid GROQ syntax
break
case 'WASM_ERROR':
// Other WASM error
break
}
}
}
Browser Support
This package works in modern browsers that support WebAssembly. The WASM modules are loaded automatically based on the environment.
// In browser. The WASM module is fetched and initialized on import.
import { lint, validate } from '@sanity-labs/groq-wasm'
const findings = lint(query)
const parsed = validate(query)
Architecture
This package uses a wrapper crate approach:
packages/groq-wasm/
├── rust/ # Rust wrapper crate
│ ├── Cargo.toml # Depends on groq-lint & groq-format
│ └── src/lib.rs # wasm-bindgen exports
├── wasm/ # Built WASM output (generated)
├── src/ # TypeScript wrappers
│ ├── index.ts # Main exports
│ ├── params.ts # Parameter extraction API
│ ├── lint.ts # Linting API
│ ├── format.ts # Formatting API
│ ├── validate.ts # Syntax validation API
│ └── wasm-loader.ts # WASM initialization
└── scripts/
└── build-wasm.sh # Build script
The Rust crate depends on upstream repos as git dependencies (no forking required).
Building from Source
To rebuild the WASM bindings:
Prerequisites
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Add WASM target
rustup target add wasm32-unknown-unknown
# Install wasm-pack
cargo install wasm-pack
Build
cd packages/groq-wasm
./scripts/build-wasm.sh
This will:
- Compile the Rust wrapper crate to WASM
- Generate JS bindings via wasm-bindgen
- Output files to
wasm/
Performance
The WASM implementation provides significant performance benefits:
- Linting: ~5-10x faster than TypeScript for complex queries
- Formatting: Near-instant formatting for most queries
- Memory: Lower memory footprint than JavaScript AST processing
License
MIT