npm.io
1.1.1 • Published 6d ago

@sanity-labs/groq-wasm

Licence
MIT
Version
1.1.1
Deps
0
Size
410 kB
Vulns
0
Weekly
0
Stars
7

@sanity-labs/groq-wasm

WASM bindings for GROQ linting and formatting.

This package provides TypeScript wrappers around:

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:

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:

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.
  • $name text 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 throws WasmError with code PARSE_ERROR, like lint() and format().

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:

  1. Compile the Rust wrapper crate to WASM
  2. Generate JS bindings via wasm-bindgen
  3. 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

Keywords