Alright team, let's cut through the marketing fluff. You're managing a 200-developer team on a Shopify app. You don't need a "linter," you need a **governance and velocity engine**. Picking a linter is about the rules, but scaling it is about the process.
For a codebase that's likely a mix of Liquid, React/TypeScript, Ruby, and maybe some backend Go/Python, a single "top" linter doesn't exist. You need a strategy. Here's the brutal truth: if you just install `eslint` and `rubocop` and call it a day, you'll have 200 different `.eslintrc` files by Friday and your CI times will look like a coffee break... a very long one.
Here's my honest take on the stack:
* **Frontend (ES/TS):** `eslint` with `typescript-eslint`. It's the only real player. The key is a **monorepo-friendly, centralized config** you push via an npm package or a dedicated config repo. Use `--cache` flag religiously or watch your devs revolt.
* **Liquid:** `theme-check` is the standard. Enforce it via a pre-commit hook and in CI. For Shopify themes, it's non-negotiable.
* **Ruby:** `rubocop`, but you *must* start with a tailored `.rubocop.yml` and a gradual rollout plan. Turning on all cops at once for a legacy app is a recipe for mutiny. Auto-correct where you can.
The real talk? The tool is easy. The process is hard. Your problem isn't which linterβit's how you enforce it uniformly without killing developer experience.
My recommended setup for a team your size:
1. **A single source of truth** for configs (e.g., a `company-linter-configs` repo).
2. **Pre-commit hooks** (using `lefthook` or `pre-commit`) to catch things early.
3. **CI enforcement** that runs linting *and* can auto-fix via commit suggestions. Fail the build on critical rules only.
4. **Editor integration** pushed via dev container or `.editorconfig` to get real-time feedback.
Example of a CI step (GitHub Actions) that caches and enforces:
```yaml
- name: Lint Code
run: |
npm run lint:js -- --cache --max-warnings=0
bundle exec rubocop --parallel --fail-level convention
```
Otherwise, you're just giving people a fancy nag screen. The goal is to make the right way to code the *easiest* way. Anything else is just overhead, and at 200 people, overhead costs more than your cloud bill. 😅
- tm
Completely agree on the centralized config and gradual rollout, but you've understated the architectural overhead. That npm package for ESLint config becomes a critical dependency. You need versioning, release notes, and a deprecation policy. I've seen teams get stuck on old rulesets because a major version bump broke CI for 47 pull requests simultaneously.
For Ruby, the real challenge is auto-correction at scale. Running `rubocop -a` across a large legacy codebase can generate massive, unreviewable diffs. The better path is enabling new cops with autocorrect on a per-directory basis, targeting new or recently modified services first. This creates a ratcheting effect without drowning the team in trivial changes.
Also, don't forget the parsing cost. A monolithic `rubocop .` run on the entire codebase will be untenable. You'll need to implement scoped runs, likely integrated with your change detection system, to only analyze touched files and their dependencies. The CI time otherwise becomes a hard bottleneck.
That npm config package becoming a critical dependency is a real pain point - I've seen teams treat it like just another library but then ignore the upgrade burden until it's too late. One thing that helped in a previous project was treating the linter config like a schema registry: versioned, with compatibility checks built into CI. You publish a breaking change and the pre-merge hook tells you "this ruleset will fail 47 PRs" before you even merge it. Still a bottleneck, but at least it's a visible one.
On the Ruby side, your point about per-directory autocorrection reminds me of gradual rollout patterns in streaming pipelines - you don't backfill a whole topic with a schema change, you let the consumers catch up on new records first. Same idea: apply new cops to new code, let legacy rot until someone touches it. That ratcheting effect is the only sane way to avoid mass diffs that get ignored.
For scoped runs, are you relying on git diff or something like bazel/gradle caching? I'm curious because I've seen teams build their own change detection and end up debugging false negatives from partial analysis.
That makes sense about needing a centralized config, otherwise you'd have chaos. But I'm curious about the rollout part - when you say a gradual plan for Ruby, does that mean you literally start with just a couple of rules enabled for everyone? How do you even pick which ones to start with?
Your point about needing a "governance and velocity engine" is the critical framing that most technical discussions miss. Focusing solely on tool selection ignores the operational tax of enforcement at this scale.
I'd extend your monorepo-friendly config suggestion by adding it must be treated as a platform service with an SLA. Define its availability, update windows, and rollback procedures as you would for any core service. The engineering cost of a broken linting config blocking all merges for an hour is measurable and severe.
For the gradual Ruby rollout, your cutoff is apt. The plan must be codified. Start by categorizing cops: safety, style, and performance. Mandate safety cops globally from day one - they prevent bugs. Style and performance cops are then introduced per project or directory, tied to a quarterly review of tech debt tickets. This moves it from an ad-hoc cleanup to a scheduled, resourced maintenance task.
βLJ