Yarn Scripts: Fixing the Windows-to-Linux Gap
5 mins read

Yarn Scripts: Fixing the Windows-to-Linux Gap

Actually, I should clarify – I spent last Tuesday night staring at a CI pipeline that refused to turn green. It’s the classic developer nightmare: everything works perfectly on my machine, but the moment it hits the staging environment, it implodes. Specifically, I was getting a cryptic permission denied error on a shell script that I knew was executable.

The culprit? My coworker committed the file from Windows.

But if you’ve ever worked in a mixed-OS team—some on Mac, some on WSL (Windows Subsystem for Linux), and the CI running on Ubuntu—you know this pain. Yarn is supposed to abstract this away, though it needs a little help at times. After fixing this for the third time in a month, I decided to stop patching leaks and actually fix the plumbing.

The CRLF and Permission Headache

Here’s the thing. Windows uses Carriage Return + Line Feed (CRLF) for line breaks. Linux uses just Line Feed (LF). When a script written in Windows gets pushed to a Linux CI runner, the interpreter often chokes. Add to that the fact that Windows file systems don’t handle executable permissions (chmod) the same way Linux does, and you have a recipe for a broken build.

I used to manually run chmod +x every time I pulled a branch. It was tedious. Then I realized I could probably bake this hygiene into my Yarn workflow.

I added a “pre-commit” or “setup” script to my package.json. It forces consistency regardless of the OS the developer is using. If you’re running WSL, this is generally mandatory.

{
  "scripts": {
    "fix:scripts": "find ./scripts -name '*.sh' -exec dos2unix {} \\; && chmod +x ./scripts/*.sh",
    "build": "yarn fix:scripts && next build",
    "deploy": "yarn fix:scripts && yarn vercel --prod"
  }
}

Now, whenever I run a build or a deploy, Yarn ensures the scripts are sanitized first. No more /bin/bash^M: bad interpreter errors at 2 AM.

Smart Caching for Heavy Builds

We’re building a dApp that interacts with Sepolia (an Ethereum testnet), and our contract compilation step was eating up half our CI minutes. It felt like burning money. Every time we pushed a PR, we were recompiling Solidity contracts that hadn’t changed.

Yarn’s cache is great for dependencies, but for build artifacts, you need to get creative. I started using a smart-caching approach. Instead of just running the compile command blindly, I wrapped it in a script that checks a hash of the contract files.

If the hash matches the last build? Skip it. If not? Compile and update the hash.

#!/bin/bash
# scripts/smart-compile.sh

CURRENT_HASH=$(find contracts -type f -print0 | sort -z | xargs -0 sha1sum | sha1sum | awk '{print $1}')
CACHE_FILE=".build-cache"

if [ -f "$CACHE_FILE" ] && [ "$(cat $CACHE_FILE)" == "$CURRENT_HASH" ]; then
  echo "✅ Contracts unchanged. Skipping compilation."
  exit 0
fi

echo "🔄 Changes detected. Compiling..."
yarn hardhat compile
echo "$CURRENT_HASH" > "$CACHE_FILE"

This dropped our average PR build time from 6 minutes to about 45 seconds. When you’re waiting on a leaderboard of PRs to get merged, that speed difference is everything. It also saved us a ton on “gas” (metaphorically speaking regarding CI credits, though literally saving gas on unnecessary testnet deploys too).

The Live URL Shortcut

Well, I hate context switching. If I’m in the terminal, I want to stay in the terminal. Opening a browser to log into a dashboard just to click “Promote to Production” feels like a failure of automation.

I mapped my production deployment directly to a Yarn command. It sounds simple, but wrapping the Vercel CLI inside Yarn allows me to enforce those script fixes I mentioned earlier before the code ever leaves my machine.

My workflow is now just:

yarn deploy:prod

Under the hood, this runs yarn vercel --prod, but only after running my linting and test suites. It’s a small efficiency hack, but it prevents that embarrassing moment where you push to prod and immediately realize you broke the layout.

Why I’m Still on Yarn in 2026

Look, I know pnpm is fast. And Bun was the shiny new toy back in ’24. But Yarn’s Plug’n’Play (PnP) feature—once I finally wrapped my head around it—has been the most stable experience for our monorepo.

We have a repo with a Next.js frontend, a Hardhat backend, and a shared UI library. Npm struggled with the hoisting. Pnpm was better, but we hit weird edge cases with ghost dependencies in our Docker builds. Yarn Berry (v4.6 at this point) just handles the workspace constraints better, probably.

For example, enforcing consistent versions across the monorepo is a one-liner in the configuration:

constraints:
  - dependency_name: "react"
    version: "19.0.0"
    workspace: "*"

Try doing that cleanly in npm without third-party tools.

Is Yarn perfect? No. The migration from v1 to Berry was painful enough that I still have scars. But right now, for a mixed-OS team trying to ship fast without breaking the build on Windows machines, it’s the tool that gets out of my way the most. And really, that’s all I want from a package manager.

Leave a Reply

Your email address will not be published. Required fields are marked *