Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions docs/settings/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,45 @@ To set the settings object, add a `settings` key to the eslint config.

### `cwd`

The working directory used to resolve `tailwindcss` and related config files. This is useful for monorepos where linting runs from the repository root but each project has its own `node_modules` and Tailwind setup.
The working directory used to resolve `tailwindcss` and related config files (`entryPoint`, `tailwindConfig`, `tsconfig`).

This path is resolved relative to the current working directory of the ESLint process. If not specified, it falls back to the current working directory of the ESLint process.
Resolved relative to the current working directory of the linter process. If not specified, defaults to the linter process working directory.

**Type**: `string`
**Default**: `undefined`

#### Monorepo support

In monorepos, the linter process working directory often differs from where `tailwindcss` is actually installed. For example, IDE extensions typically run the linter from the repository root, while CLI scripts run from the package directory.

The plugin handles this automatically: when `tailwindcss` cannot be found at the configured `cwd`, it falls back to resolving from the directory of the file being linted and walks up the directory tree. This means **most monorepo setups work without any `cwd` configuration**.

You only need to set `cwd` explicitly when paths like `entryPoint` or `tailwindConfig` can't be resolved from the auto-detected location.

```js
// eslint.config.js — per-package settings in a monorepo
export default [
{
files: ["packages/website/**/*.{js,jsx,ts,tsx}"],
settings: {
"better-tailwindcss": {
cwd: "./packages/website",
entryPoint: "./src/globals.css"
}
}
},
{
files: ["packages/app/**/*.{js,jsx,ts,tsx}"],
settings: {
"better-tailwindcss": {
cwd: "./packages/app",
entryPoint: "./src/globals.css"
}
}
}
];
```

<br/>

### `detectComponentClasses`
Expand Down
6 changes: 3 additions & 3 deletions src/async-utils/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ export function resolveCss(ctxOrPath: AsyncContext | string | undefined, pathOrC
}
}

export function resolveJson(path: string, cwd: string): string {
export function resolveJson(path: string, cwd: string): string | undefined {
try {
return jsonResolver.resolveSync({}, cwd, path) || path;
return jsonResolver.resolveSync({}, cwd, path) || undefined;
} catch {
return path;
return undefined;
}
}
16 changes: 14 additions & 2 deletions src/utils/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,23 @@ export function createRule<
const { messageStyle } = options;

// #361#issuecomment-4227041592
const cwd = options.cwd
let cwd = options.cwd
? resolve(ctx.cwd, options.cwd)
: ctx.cwd;

const packageJsonPath = resolveJson("tailwindcss/package.json", cwd);
let packageJsonPath = resolveJson("tailwindcss/package.json", cwd);

// Monorepo fallback: when tailwindcss is not found at the configured cwd
// (e.g. IDE running from repo root), try resolving from the file being linted.
// The file is always inside the project that has tailwindcss installed, so the
// resolver will find it by walking up from the file's directory.
if(!packageJsonPath && ctx.filename){
const fileDir = dirname(ctx.filename);
packageJsonPath = resolveJson("tailwindcss/package.json", fileDir);
if(packageJsonPath){
cwd = dirname(dirname(dirname(packageJsonPath)));
}
}

if(!packageJsonPath){
warnOnce(`Tailwind CSS is not installed. Disabling rule ${ctx.id}.`);
Expand Down
75 changes: 75 additions & 0 deletions tests/unit/monorepo-resolution.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

import { Linter } from "eslint";
import { afterAll, beforeAll, describe, expect, it } from "vitest";

import { noUnknownClasses } from "better-tailwindcss:rules/no-unknown-classes.js";


// Simulates a monorepo where:
// /monorepo-root/ <-- linter cwd (no tailwindcss here)
// /monorepo-root/packages/website/node_modules/tailwindcss/ <-- installed here
// /monorepo-root/packages/website/src/index.tsx <-- file being linted

const MONOREPO_ROOT = join(tmpdir(), "eslint-btw-monorepo-test");
const WORKSPACE = join(MONOREPO_ROOT, "packages", "website");
const TAILWIND_PKG = join(WORKSPACE, "node_modules", "tailwindcss");

beforeAll(() => {
rmSync(MONOREPO_ROOT, { force: true, recursive: true });

mkdirSync(join(WORKSPACE, "src"), { recursive: true });

mkdirSync(TAILWIND_PKG, { recursive: true });
writeFileSync(join(TAILWIND_PKG, "package.json"), JSON.stringify({
name: "tailwindcss",
style: "index.css",
version: "4.0.0"
}));
writeFileSync(join(TAILWIND_PKG, "index.css"), "");
mkdirSync(join(TAILWIND_PKG, "dist"), { recursive: true });
writeFileSync(join(TAILWIND_PKG, "theme.css"), "");
});

afterAll(() => {
rmSync(MONOREPO_ROOT, { force: true, recursive: true });
});


function lintFile(cwd: string, filename: string) {
const linter = new Linter({ configType: "flat", cwd });
return linter.verify(
`export default () => <div className="flex" />;`,
{
languageOptions: {
parserOptions: { ecmaFeatures: { jsx: true } }
},
plugins: {
"rule-to-test": { rules: { [noUnknownClasses.name]: noUnknownClasses.rule } }
},
rules: { [`rule-to-test/${noUnknownClasses.name}`]: "warn" }
},
{ filename }
);
}


describe("monorepo resolution", () => {

it("should resolve tailwindcss from the file's directory when linter cwd is the monorepo root", () => {
const messages = lintFile(MONOREPO_ROOT, join(WORKSPACE, "src", "index.tsx"));

const installError = messages.find(m => m.message.includes("Tailwind CSS is not installed"));
expect(installError).toBeUndefined();
});

it("should resolve tailwindcss when linter cwd matches the workspace", () => {
const messages = lintFile(WORKSPACE, join(WORKSPACE, "src", "index.tsx"));

const installError = messages.find(m => m.message.includes("Tailwind CSS is not installed"));
expect(installError).toBeUndefined();
});

});