A Dagger module, written in Dang, that lints your JavaScript and TypeScript projects with ESLint, using each project's own ESLint configuration and version.
Requires Dagger v1.0.0-beta.15 or later.
dagger install github.com/dagger/eslintThe module lints each ESLint project in your workspace separately. A project is a directory holding an ESLint config file:
- flat config:
eslint.config.js,.mjs,.cjs,.ts,.mtsor.cts - legacy config:
.eslintrc,.eslintrc.js,.cjs,.json,.yamlor.yml
A project is keyed by its path from the workspace root (. for the root).
List the projects visible from where you stand:
dagger list eslint-projects -aDiscovery scans the whole workspace once, reading only file names and
config text; it never runs ESLint or Node, so listing stays fast, and what is
a project does not depend on where you run dagger. That has limits:
node_modulesdirectories are never searched.- A config inside a directory that the nearest enclosing config ignores globally is not a project, and neither is anything below it. A config in between starts a fresh scope: a grandparent's ignores do not reach past it. This is how ESLint 10 walks a tree. ESLint 9 and 8 (flat config) apply only the config found from where they run, so the module's split into projects gives the same answer as running ESLint inside each project directory. Either way no file is linted by nobody, and none twice.
- Only literal string patterns are read: an object holding just
ignores(and optionallyname), orglobalIgnores([...]). Patterns built at runtime, ignores in an object that also hasfiles, and legacy.eslintrcignorePatternsare not seen. - A config given only in
package.json(eslintConfig), through--config, or under another file name is not found. - A directory without its own config is linted as part of the project that encloses it.
Each project is linted from its own directory. Projects nested inside it are
skipped there (--ignore-pattern <nested>/**) and linted on their own, with
their own config, so no file is linted twice.
Which projects you see depends on where you run dagger:
- At a project root: that project and the projects below it.
- Inside a project's subdirectory: the enclosing project, plus any projects below where you stand. A directory the project ignores counts as its subdirectory, even if it holds a config of its own.
- Outside any project: the projects below you.
So you don't need a flag to lint the project you are working in:
cd app/src && dagger check # lints the app projectEach project runs the ESLint its own dependencies install:
- Where: dependencies are installed at the nearest workspace root at or
above the project (a
pnpm-workspace.yaml, or apackage.jsonwith"workspaces"), else the nearest directory with a lockfile, else the nearestpackage.json. That tree is mounted, soworkspace:andcatalog:dependencies and shared configs resolve. - With what: the
packageManagersetting, or when it is empty, the"packageManager"field of thatpackage.json, else the lockfile (pnpm-lock.yaml,yarn.lock,bun.lock/bun.lockb), else npm. pnpm and yarn run through corepack, which is installed first on images that no longer ship it (Node 25 and later), and use the versionpackageManagernames. - Cached: only the files the install reads (every
package.json, lockfiles,pnpm-workspace.yaml,.npmrc,.yarnrc*,.yarn/releases, plugins and patches,patches/, and the directories thatfile:,link:andportal:dependencies point to) are mounted for the install, so editing source does not re-run it. When apackage.jsonmarks a dependencyinjected, or a local dependency lies outside the install root, the install gets the full source instead. The package manager caches, the pnpm store and corepack's downloads are cache volumes. Browser downloads (Playwright, Puppeteer, Cypress) and git hook installs are skipped. - Which ESLint: the nearest
node_modules/.bin/eslintfrom the project up to the install root, oryarn eslintunder Yarn Plug'n'Play. If the project doesn't install ESLint, the check fails and says so. ESLint runs with its default (single-threaded) concurrency; projects are linted in parallel with each other.
A project with no package.json at or above it installs nothing, and npx
fetches the latest ESLint, so a standalone ESLint config works without a Node
project.
Lifecycle scripts run during the install without the source mounted. If one
needs source files, pass --ignore-scripts in installFlags.
Failures name the project and the step: install failed (pnpm install, exit 1) with the end of the installer's output, or eslint failed (exit 1) with
ESLint's report.
| Address | Runs |
|---|---|
eslint/projects/lint |
ESLint in each project (@check) |
dagger check # every check in the workspace
dagger check --eslint # every ESLint project
dagger check eslint/projects/lint # the same, by address
dagger check --eslint-project=app # one project (repeatable)
dagger check --check lint # checks named lint, in every module
dagger check -l --all --eslint # list one line per project
dagger check -l --all --eslint -f=cli # ...as flags you can paste backThe selected projects are linted in parallel, and every failing project is reported.
Run the check with dagger check, in CI especially: dagger call on a check
function does not fail the command when the check fails.
The flags for this module (see dagger check --help):
| Flag | Selects |
|---|---|
--eslint, --by-eslint |
checks from this module |
--eslint-project=PATH |
one project (repeatable) |
--eslint-projects |
every project |
--check NAME |
checks with that name, in every module |
The short flag --eslint-project stays as long as no other installed module
has an item type with the same name. dagger check --help lists the flags in
effect.
Each project has a fix function that runs eslint --fix and returns the
changes as a Changeset, rooted at your working directory. It keeps the
fixes even when problems ESLint cannot fix remain, and leaves those for lint
to report. fix is not a generator, so it adds no check to dagger check.
Apply a project's fixes from the CLI with a Dagger script (run it from the workspace root, since the changes are rooted where you run it):
dagger -c 'eslint | projects | get app | fix | export .'projects(ws) returns a collection. Use keys, get(key:) and
subset(keys:) to select projects, and batch to run a function over the
selection:
let projects = eslint.projects(ws)
projects.keys # ["app", "app/packages/ui"]
run(projects.batch.lint(ws)) # lint every project
run(projects.subset(keys: ["app"]).batch.lint(ws))
run(projects.get(key: "app").lint(ws)) # one project
projects.get(key: "app").fix(ws) # its fixes, as a Changeset
A check called through a dependency returns a Check that has not run yet.
Wrap it to run it and raise its failure:
let run(check: Check!): Void {
if (check.pass == false) {
raise check.error.message ?? "check failed"
}
null
}
Set these in your workspace dagger.toml:
[modules.eslint]
source = "github.com/dagger/eslint"
settings.baseImageAddress = "node:22" # default: node:25-alpine; any image with Node
settings.packageManager = "pnpm" # default: "" (detect); npm, pnpm, yarn or bun
settings.installFlags = ["--ignore-scripts"] # default: []; appended to the install command
settings.environment = ["NODE_OPTIONS=--max-old-space-size=4096"] # default: []; KEY=VALUE for ESLintOr from the CLI:
dagger settings eslint baseImageAddress node:22 # set
dagger settings -u eslint baseImageAddress # unset, back to the default