Frontend Linting: Fixing Drift Between Local And CI Environments
Hey folks! Today, we're diving deep into a common issue in monorepo setups: the potential drift between frontend linting scripts and root CI linting behavior. This can lead to a world of headaches, where your code passes linting locally but fails miserably in CI, or vice versa. Let’s break down why this happens, what problems it causes, and how we can fix it!
Summary
The core issue is that the frontend package.json
often contains a local "lint": "eslint ."
script. While seemingly convenient, this script can behave differently from the linting process run in the Continuous Integration (CI) environment, which typically operates from the repository root. This discrepancy arises because CI might use different ESLint configurations or glob patterns, leading to inconsistent linting results. The goal here is to ensure consistency so that what you see locally is what CI sees, preventing surprises and wasted CI cycles.
Problem Details
To illustrate the problem, let's consider a typical setup in a monorepo structure.
Frontend package.json
:
{
"scripts": {
"lint": "eslint ."
}
}
This script is pretty straightforward: it tells ESLint to lint all files in the current directory (or its subdirectories). However, this simplicity masks a potential issue.
CI Workflow (.github/workflows/ci.yml
):
- name: Run linter
run: pnpm lint # Runs from root, not from apps/frontend
In the CI workflow, linting is often executed from the root of the repository. This means the root package.json
's lint script is used, not the one in apps/frontend
.
Root package.json
:
{
"scripts": {
"lint": "eslint ." # Lints entire monorepo
}
}
Here’s where the conflict arises. The root lint script is designed to lint the entire monorepo, while the frontend script focuses solely on the frontend application.
Why This Can Cause Issues
Let’s delve into the specific reasons why this setup can lead to problems.
1. Different ESLint Configs Apply
This is a big one! The linting process's behavior is heavily influenced by the ESLint configuration files it uses.
- Running from root: When linting is done from the root, it typically uses the root
eslint.config.js
. This configuration often contains a comprehensive set of rules designed to enforce code quality standards across the entire monorepo. - Running from
apps/frontend
: If you run linting from theapps/frontend
directory, it might use a localapps/frontend/eslint.config.js
. This local configuration might be more minimal or tailored specifically for the frontend codebase. - Result: This difference can lead to different linting results for the same files. For instance, a file might pass linting with the frontend-specific config but fail when linted with the root config, and vice-versa.
2. Different Working Directories
The working directory from which the linting command is executed also plays a crucial role.
cd apps/frontend && pnpm lint
: When you navigate into theapps/frontend
directory and runpnpm lint
, ESLint interprets paths relative to theapps/frontend/
directory. This means it will only lint files within the frontend application.pnpm lint
from root: If you runpnpm lint
from the root, ESLint interprets paths relative to the repository root. This results in the entire monorepo being linted, including the frontend application.
3. Inconsistent Developer Workflow
The discrepancy between local and CI linting can significantly disrupt the developer workflow.
Imagine a developer working in the apps/frontend/
directory. They might run:
cd apps/frontend
pnpm lint # Uses frontend-specific config
This command uses the frontend-specific ESLint configuration and lints only the frontend code. However, CI does something different:
pnpm lint # Uses root config from root directory
CI uses the root ESLint configuration and lints the entire monorepo. This inconsistency can lead to the dreaded "works on my machine" syndrome, where code passes linting locally but fails in CI.
4. Pre-commit Hook Confusion
Pre-commit hooks are scripts that run automatically before you commit code. They're a great way to catch issues early in the development process. However, the discrepancy between local and CI linting can also affect pre-commit hooks.
Pre-commit hooks typically run from the root of the repository and use the root ESLint configuration. This is good because it aligns with the CI environment. However, if developers are accustomed to running pnpm lint
from the frontend directory, they might be surprised when the pre-commit hook flags issues that they didn't see locally.
Current Impact
Let's assess the impact of this issue on different aspects of the development process.
Development:
- Developers might run
pnpm lint
from the frontend directory, expecting it to catch all potential issues. - They might get different results than CI due to the different ESLint configurations and working directories.
- This can lead to the "works on my machine" syndrome, where code passes linting locally but fails in CI.
CI:
- CI might fail on issues that pass locally, leading to wasted CI cycles and frustration.
- Conversely, it's also possible (though less common) for CI to pass while local linting fails, which can mask potential issues.
Severity: The severity of this issue ranges from Low (if the frontend ESLint configuration extends the root configuration properly) to Medium (if the configurations diverge significantly).
Recommended Solutions
Now, let's explore some solutions to address this problem. We have a few options, each with its pros and cons.
Option A: Remove Frontend Lint Script (Recommended)
This is our top recommendation. The idea is to remove the "lint"
script from apps/frontend/package.json
altogether. This forces developers to use a consistent linting command, regardless of their current working directory.
From root:
pnpm --filter @matrix-academy/frontend lint
# Or just: pnpm lint (lints everything)
This command uses pnpm
's filtering capabilities to run the lint
script defined in the root package.json
, but only for the @matrix-academy/frontend
package. Alternatively, you can simply run pnpm lint
from the root, which will lint the entire monorepo.
Update frontend README:
To ensure developers know how to lint the frontend code, we need to update the frontend README.md
file.
# Lint code
pnpm lint # From root (lints all packages)
# Or specifically frontend:
pnpm --filter @matrix-academy/frontend lint
This clearly explains how to lint the frontend code from the root of the repository.
Pros:
- Forces consistent workflow: This approach ensures that everyone uses the same linting command, regardless of their working directory.
- No confusion about which config applies: By removing the frontend lint script, we eliminate the possibility of using the wrong ESLint configuration.
- Matches backend pattern (if the backend also uses root lint): This approach promotes consistency across the monorepo.
Cons:
- Slightly more verbose command: The
pnpm --filter
command is a bit longer than simply runningpnpm lint
from the frontend directory.
Option B: Make Frontend Script Use Root Config
Another option is to modify the frontend lint script to explicitly use the root ESLint configuration. This can be achieved by changing the working directory before running the lint command.
{
"scripts": {
"lint": "cd ../.. && pnpm lint"
}
}
This script changes the working directory to the repository root (../..
) before running pnpm lint
. This ensures that the root ESLint configuration is used, even when running the script from the frontend directory.
Pros:
- Can still run
pnpm lint
from frontend: Developers can still use the convenientpnpm lint
command from the frontend directory. - Uses root config consistently: This approach ensures that the root ESLint configuration is always used.
Cons:
- Hacky, changes working directory: This approach feels a bit hacky and might not be the most elegant solution.
- May confuse paths in error messages: Changing the working directory can sometimes lead to confusing paths in error messages.
Option C: Document the Difference
If we decide to keep both the frontend and root lint scripts, it's crucial to document the difference between them clearly. This involves explaining when to use each script and highlighting the potential for inconsistencies.
In apps/frontend/README.md
:
## Linting
**⚠️ Important**: Always lint from repository root for CI-consistent results:
```bash
# From repository root
pnpm lint
# Or specifically frontend:
pnpm --filter @matrix-academy/frontend lint
The local pnpm lint
script exists for convenience but may use different rules.
This documentation emphasizes the importance of linting from the repository root for CI consistency and warns developers about the potential for differences when using the local lint script.
**Pros**:
- **Flexibility**: This approach allows developers to choose which lint script to use.
- **Documents the issue**: The documentation clearly explains the potential for inconsistencies.
**Cons**:
- **Still allows divergence**: This approach doesn't prevent developers from using the local lint script and encountering inconsistencies.
- **Relies on developers reading docs**: Developers need to read and understand the documentation to avoid potential issues.
### Option D: Ensure Configs Are Identical
Another approach is to ensure that the frontend ESLint configuration extends the root configuration exactly. This means that both configurations should produce the same linting results.
If the configurations are properly aligned, both commands should give the same results. This ensures consistency regardless of where the linting command is executed.
**Pros**:
- **No workflow changes needed**: This approach doesn't require any changes to the developer workflow.
- **Both commands work correctly**: Both the frontend and root lint scripts should produce the same results.
**Cons**:
- **Requires fixing ESLint config issues first**: This approach requires addressing any existing differences between the ESLint configurations.
- **Maintenance burden to keep in sync**: It requires ongoing effort to ensure that the configurations remain synchronized.
## Recommended Approach
Our recommended approach is to combine **Option A** (Remove frontend lint script) with **Option D** (fix config). This provides the most robust and consistent solution.
Here's the recommended implementation plan:
1. Fix dual ESLint config issue.
2. Remove `"lint"` script from `apps/frontend/package.json`.
3. Document use of `pnpm lint` or `pnpm --filter` from root.
4. Update CI if needed (already runs from root).
## Acceptance Criteria
To ensure that the solution is effective, we need to define clear acceptance criteria.
- [ ] Linting behavior is identical whether run from root or frontend directory.
- [ ] CI uses the same lint command developers use.
- [ ] Documentation clearly explains the lint workflow.
- [ ] Pre-commit hooks use the same config as manual linting.
- [ ] No surprises where local lint passes but CI fails.
## When to Implement
**Priority**: The priority of this issue is **Low** if the ESLint configurations are already aligned, and **Medium** if we're experiencing lint inconsistencies.
**Implement with**: This fix should be implemented in conjunction with addressing any underlying ESLint configuration issues.
## Additional Context
- This is a common monorepo gotcha, so it's not something we're alone in experiencing.
- The backend might have the same issue if it has a local lint script, so it's worth checking.
- Pre-commit hooks already run from the root, which is a good thing!
- CI runs from the root, which is also good!
- The main issue is developer workflow consistency, making sure everyone's on the same page.
## Testing
After implementing the fix, it's crucial to verify that it's working correctly. Here's a simple testing procedure:
```bash
# From root
pnpm lint
# Note output
# From frontend
cd apps/frontend
pnpm lint
# Should have identical output
# Both should catch the same issues
This testing ensures that the linting behavior is consistent regardless of the working directory and that both the root and frontend linting processes catch the same issues.
Conclusion
Alright, guys, that's a wrap on addressing the frontend linting discrepancies! By removing the frontend lint script and ensuring consistent ESLint configurations, we can create a smoother and more predictable development experience. This not only saves time and frustration but also helps maintain code quality across the entire monorepo. Keep coding, keep linting, and keep those CI pipelines green!