DEV Community

Cover image for Stop Breaking Your Map Styles in CI
Navid Nabavi
Navid Nabavi

Posted on

Stop Breaking Your Map Styles in CI

You open a style JSON. It's 2,000+ lines. You rename a source. You update the layers you remember. You deploy. Two days later someone asks why the rivers are gone. You check the map config — looks fine. You check the source URLs — fine. You grep the codebase, nothing obvious. Eventually you diff the style JSON against last week's version and find it: one layer still referencing the old source name. One line. Two days. The file is valid JSON. It passes schema checks. The map mostly works. But "mostly" is doing a lot of work there.


Mapbox Studio and Maputnik catch a lot while you're editing. But they're visual tools — they don't cover every custom source URL or every expression that degrades performance at scale. And more importantly: they're not in your CI pipeline.


Once a style file lands in a git repo, things drift. Someone edits it in VS Code. A build script modifies it programmatically. A PR renames a source but misses one of the three layers referencing it. In production, vector tiles load but features are invisible and no one knows why.

There's no ESLint for map styles. No cargo clippy. No automated gate between "looks fine locally" and "merged to main."

styl is that gate.

What styl does

Overview of project

styl is a CLI linter, validator, and formatter for MapLibre GL and Mapbox GL style JSON — built to run in CI, pre-commit hooks, and local development on raw files.

$ styl check style.json

error[E003] sources.roads: vector source missing required field "url" or "tiles"
  --> style.json

warning[W002] layers[9].layout.visibility: layer "Building" is permanently invisible
  --> style.json
  hint: remove the layer or set visibility to "visible" if it should be shown

warning[W011] layers[3].filter: layer "Landuse" uses deprecated legacy filter syntax
  --> style.json
  hint: migrate to expression-based filters: https://maplibre.org/maplibre-style-spec/expressions/
Enter fullscreen mode Exit fullscreen mode

Both errors and warnings exit 1 — use .stylrc to tune severity per rule if a warning shouldn't block your build.

Three categories:

  • Spec violations (E-codes) — missing required fields, broken source references, source-layer mismatch (e.g. style says roads but the tile has road), layers pointing to non-existent sources
  • Best-practice warnings (W-codes) — permanently invisible layers (which still consume draw calls), duplicate IDs, legacy filter syntax, deeply nested expressions, performance anti-patterns
  • Formatting — canonical key ordering via styl fmt, with --check mode that exits 1 if anything would change

The formatter matters beyond aesthetics: unsorted keys create noisy PR diffs where real changes hide behind key-order churn. styl fmt makes diffs clean and reviewable.

Drop It Into CI

jobs:
  style-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install styl
        run: curl -fsSL https://raw.githubusercontent.com/navidnabavi/styl/main/install.sh | bash
      - name: Validate map styles
        run: styl check --format github style.json
Enter fullscreen mode Exit fullscreen mode

For locked-down CI environments, use Homebrew instead: brew install navidnabavi/tap/styl.
More integrations are on the way!

The --format github flag emits GitHub Actions annotations — errors appear inline on the diff. Use --format json for GitLab CI or custom tooling.

For pre-commit:

styl fmt --check style.json && styl check style.json
Enter fullscreen mode Exit fullscreen mode

Install

# macOS / Linux via Homebrew
brew tap navidnabavi/tap
brew install styl

# One-liner (no Rust required)
curl -fsSL https://raw.githubusercontent.com/navidnabavi/styl/main/install.sh | bash

# Pinned version with Rust
cargo install --git https://github.com/navidnabavi/styl --tag v0.0.3
Enter fullscreen mode Exit fullscreen mode

Stdin is supported too — pipe-friendly for GIS pipelines:

cat style.json | styl check --stdin
Enter fullscreen mode Exit fullscreen mode

Per-Project Config

Not every warning fits every project. Drop a .stylrc at your project root:

[rules]
W002 = "error"   # invisible layers are a hard error for us
W003 = "off"     # some layers intentionally toggled at runtime
W011 = "warn"    # legacy filters: warn but don't block

[format]
indent = 4
Enter fullscreen mode Exit fullscreen mode

styl walks up the directory tree to find it — monorepo-friendly. The file is spec-neutral: same config works whether you're targeting MapLibre or Mapbox.

Both Specs Supported

styl defaults to MapLibre GL Style Spec v8. Pass --spec mapbox for Mapbox GL.

Spec-specific divergence points — expression support differences, deprecated properties — are tracked in issues. Contributions filling those gaps are welcome.

What's Coming

  • VS Code extension — inline diagnostics as you type, no CLI required
  • Zed extension — same, for Zed users
  • Watch modestyl watch style.json for live feedback during local development

Adding a new lint rule is ~50 lines implementing a single trait — see src/linter/rules/visibility.rs as an example. If you have a pattern worth catching, open an issue or send a PR.


The next time a source rename breaks production tiles, you'll wish this was already in your pipeline.

GitHub →

If it saves you time, a ⭐ on the repo goes a long way.

Top comments (0)