<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Navid Nabavi</title>
    <description>The latest articles on DEV Community by Navid Nabavi (@navidnabavi).</description>
    <link>https://dev.to/navidnabavi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1261448%2F8280b1e9-6c41-41b4-be0d-65dcf10bdc95.jpeg</url>
      <title>DEV Community: Navid Nabavi</title>
      <link>https://dev.to/navidnabavi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/navidnabavi"/>
    <language>en</language>
    <item>
      <title>Stop Breaking Your Map Styles in CI</title>
      <dc:creator>Navid Nabavi</dc:creator>
      <pubDate>Fri, 22 May 2026 10:52:23 +0000</pubDate>
      <link>https://dev.to/navidnabavi/stop-breaking-your-map-styles-in-ci-37cc</link>
      <guid>https://dev.to/navidnabavi/stop-breaking-your-map-styles-in-ci-37cc</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://www.mapbox.com/mapbox-studio" rel="noopener noreferrer"&gt;Mapbox Studio&lt;/a&gt; and &lt;a href="https://maputnik.github.io/" rel="noopener noreferrer"&gt;Maputnik&lt;/a&gt; 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.&lt;/p&gt;




&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;There's no &lt;strong&gt;ESLint&lt;/strong&gt; for map styles. No &lt;code&gt;cargo clippy&lt;/code&gt;. No automated gate between "looks fine locally" and "merged to main."&lt;/p&gt;

&lt;p&gt;&lt;code&gt;styl&lt;/code&gt; is that gate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What styl does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgnuounwiphkojquwpl74.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgnuounwiphkojquwpl74.png" alt="Overview of project" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;styl&lt;/code&gt; 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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;styl check style.json
&lt;span class="go"&gt;
error[E003] sources.roads: vector source missing required field "url" or "tiles"
&lt;/span&gt;&lt;span class="gp"&gt;  --&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;style.json
&lt;span class="go"&gt;
warning[W002] layers[9].layout.visibility: layer "Building" is permanently invisible
&lt;/span&gt;&lt;span class="gp"&gt;  --&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;style.json
&lt;span class="go"&gt;  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
&lt;/span&gt;&lt;span class="gp"&gt;  --&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;style.json
&lt;span class="go"&gt;  hint: migrate to expression-based filters: https://maplibre.org/maplibre-style-spec/expressions/
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both errors and warnings exit &lt;code&gt;1&lt;/code&gt; — use &lt;code&gt;.stylrc&lt;/code&gt; to tune severity per rule if a warning shouldn't block your build.&lt;/p&gt;

&lt;p&gt;Three categories:&lt;/p&gt;

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

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

&lt;h2&gt;
  
  
  Drop It Into CI
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;style-check&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install styl&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;curl -fsSL https://raw.githubusercontent.com/navidnabavi/styl/main/install.sh | bash&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Validate map styles&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;styl check --format github style.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For locked-down CI environments, use Homebrew instead: &lt;code&gt;brew install navidnabavi/tap/styl&lt;/code&gt;.&lt;br&gt;
More integrations are on the way!&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;--format github&lt;/code&gt; flag emits GitHub Actions annotations — errors appear inline on the diff. Use &lt;code&gt;--format json&lt;/code&gt; for GitLab CI or custom tooling.&lt;/p&gt;

&lt;p&gt;For pre-commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;styl &lt;span class="nb"&gt;fmt&lt;/span&gt; &lt;span class="nt"&gt;--check&lt;/span&gt; style.json &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; styl check style.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# macOS / Linux via Homebrew&lt;/span&gt;
brew tap navidnabavi/tap
brew &lt;span class="nb"&gt;install &lt;/span&gt;styl

&lt;span class="c"&gt;# One-liner (no Rust required)&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/navidnabavi/styl/main/install.sh | bash

&lt;span class="c"&gt;# Pinned version with Rust&lt;/span&gt;
cargo &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--git&lt;/span&gt; https://github.com/navidnabavi/styl &lt;span class="nt"&gt;--tag&lt;/span&gt; v0.0.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stdin is supported too — pipe-friendly for GIS pipelines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;style.json | styl check &lt;span class="nt"&gt;--stdin&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Per-Project Config
&lt;/h2&gt;

&lt;p&gt;Not every warning fits every project. Drop a &lt;code&gt;.stylrc&lt;/code&gt; at your project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[rules]&lt;/span&gt;
&lt;span class="py"&gt;W002&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"error"&lt;/span&gt;   &lt;span class="c"&gt;# invisible layers are a hard error for us&lt;/span&gt;
&lt;span class="py"&gt;W003&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"off"&lt;/span&gt;     &lt;span class="c"&gt;# some layers intentionally toggled at runtime&lt;/span&gt;
&lt;span class="py"&gt;W011&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"warn"&lt;/span&gt;    &lt;span class="c"&gt;# legacy filters: warn but don't block&lt;/span&gt;

&lt;span class="nn"&gt;[format]&lt;/span&gt;
&lt;span class="py"&gt;indent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;styl&lt;/code&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Both Specs Supported
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;styl&lt;/code&gt; defaults to &lt;strong&gt;MapLibre GL Style Spec v8&lt;/strong&gt;. Pass &lt;code&gt;--spec mapbox&lt;/code&gt; for Mapbox GL.&lt;/p&gt;

&lt;p&gt;Spec-specific divergence points — expression support differences, deprecated properties — are tracked in &lt;a href="https://github.com/navidnabavi/styl/issues" rel="noopener noreferrer"&gt;issues&lt;/a&gt;. Contributions filling those gaps are welcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Coming
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;VS Code extension&lt;/strong&gt; — inline diagnostics as you type, no CLI required&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zed extension&lt;/strong&gt; — same, for Zed users&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch mode&lt;/strong&gt; — &lt;code&gt;styl watch style.json&lt;/code&gt; for live feedback during local development&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding a new lint rule is ~50 lines implementing a single trait — see &lt;a href="https://github.com/navidnabavi/styl/blob/main/src/linter/rules/visibility.rs" rel="noopener noreferrer"&gt;&lt;code&gt;src/linter/rules/visibility.rs&lt;/code&gt;&lt;/a&gt; as an example. If you have a pattern worth catching, open an issue or send a PR.&lt;/p&gt;




&lt;p&gt;The next time a source rename breaks production tiles, you'll wish this was already in your pipeline.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/navidnabavi/styl" rel="noopener noreferrer"&gt;GitHub →&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If it saves you time, a ⭐ on the repo goes a long way.&lt;/p&gt;

</description>
      <category>mapbox</category>
      <category>maplibre</category>
      <category>opensource</category>
      <category>rust</category>
    </item>
  </channel>
</rss>
