Skip to content

Commit 3e2b2d0

Browse files
authored
Add ESLint (#48)
1 parent 305b552 commit 3e2b2d0

7 files changed

Lines changed: 377 additions & 9 deletions

File tree

.dockerignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
!.clang-format
33
!.editorconfig
44
!entry.ts
5+
!eslint.config.js
56
!ruff.toml
67
!stylesheet.xml
78
!svgo.config.js

Dockerfile

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ RUN tsc \
1616
# eclipse-temurin:21-alpine java --list-modules`, then removing modules by trial
1717
# and error until `make test` throws ClassNotFoundException. When first
1818
# implemented, this custom JRE reduced our image size from 574 MB to 469 MB
19-
FROM amazoncorretto:21.0.6-alpine3.21 as jre
19+
FROM amazoncorretto:21.0.6-alpine3.21 AS jre
2020
RUN apk add binutils && jlink \
2121
--add-modules java.se,jdk.compiler,jdk.unsupported \
2222
--compress zip-6 \
@@ -57,10 +57,27 @@ echo 'source /black21-venv/bin/activate && black "$@"' > /usr/bin/black21
5757
chmod +x /usr/bin/black21
5858

5959
# Install Node dependencies
60+
#
61+
# We stay on eslint-plugin-unicorn 56.0.1 because 57+ removed support for
62+
# importing this plugin into the ESLint config as CommonJS. (We use CommonJS
63+
# instead of ESM in the ESLint config mainly because the latter requires a new
64+
# local package.json file that declares `"type": "module"`, which interferes
65+
# with other tools like SVGO). I couldn't get the `deasync` hack to work - it
66+
# just hung forever. TODO: Try updating this plugin after updating Node.js to
67+
# v22+, which has experimental support for synchronously require()-ing ESM?
68+
# https://github.com/sindresorhus/eslint-plugin-unicorn/releases/tag/v57.0.0
69+
# https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c
70+
# https://nodejs.org/en/blog/announcements/v22-release-announce#support-requireing-synchronous-esm-graphs
71+
# https://github.com/eslint/eslint/issues/13684#issuecomment-722949152
6072
npm install -g \
6173
@prettier/plugin-xml@3.4.1 \
62-
prettier@3.3.3 \
63-
svgo@3.3.2
74+
eslint@9.23.0 \
75+
eslint-plugin-jsdoc@50.6.9 \
76+
eslint-plugin-sort-keys@2.3.5 \
77+
eslint-plugin-unicorn@56.0.1 \
78+
prettier@3.5.3 \
79+
svgo@3.3.2 \
80+
typescript-eslint@8.29.0
6481

6582
# Install Scala dependencies
6683
wget https://github.com/coursier/coursier/releases/download/v2.1.17/coursier -O /bin/coursier
@@ -108,3 +125,4 @@ COPY --from=entry /entry.js /entry
108125
# https://github.com/coursier/coursier/issues/1955#issuecomment-956697764
109126
ENV COURSIER_CACHE=/tmp/coursier-cache
110127
ENV COURSIER_JVM_CACHE=/tmp/coursier-jvm-cache
128+
ENV NODE_PATH=/usr/local/lib/node_modules

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# pre-commit hooks
22

3-
This repo currently contains a single [pre-commit](https://pre-commit.com/) hook that internally runs several code formatters in parallel.
3+
This repo currently contains a single [pre-commit](https://pre-commit.com/) hook that internally runs several code formatters in parallel:
44

5-
- [Prettier](https://github.com/prettier/prettier) v3.3.3 for CSS, HTML, JS, JSX, Markdown, Sass, TypeScript, XML, YAML
5+
- [Prettier](https://github.com/prettier/prettier) v3.5.3 for CSS, HTML, JS, JSX, Markdown, Sass, TypeScript, XML, YAML
6+
- [ESLint](https://eslint.org/) v9.23.0 for JS, TypeScript
67
- [Ruff](https://docs.astral.sh/ruff/) v0.7.3 for Python 3
78
- [Black](https://github.com/psf/black) v21.12b0 for Python 2
89
- [autoflake](https://github.com/myint/autoflake) v1.7.8 for Python <!-- TODO: Upgrade to v2+, restrict to Python 2, and reenable Ruff rule F401 once our Python 3 repos that were converted from Python 2 no longer use type hint comments: https://github.com/PyCQA/autoflake/issues/222#issuecomment-1419089254 -->
@@ -23,6 +24,8 @@ This repo currently contains a single [pre-commit](https://pre-commit.com/) hook
2324
- Replacing empty Python collections like `list()` with literal equivalents
2425
- Replacing empty Kotlin collections like `arrayOf()` with `empty` equivalents
2526

27+
To minimize developer friction, we enable only rules whose violations can be fixed automatically and disable all rules whose violations require manual correction.
28+
2629
We run this hook on developer workstations and enforce it in CI for all production repos at Duolingo.
2730

2831
## Usage

entry.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import { createInterface } from "readline";
1010
*/
1111
const EMPTY_FILE = "/emptyfile";
1212

13+
/** Minified JS files to exclude from formatting */
14+
const MINIFIED_JS_REGEX = /\bmin\b|\.(custom|pack)\./;
15+
1316
/** CLI options to use in all Prettier invocations */
1417
const PRETTIER_OPTIONS = [
1518
"--arrow-parens",
@@ -27,6 +30,8 @@ const PRETTIER_OPTIONS = [
2730
"warn",
2831
"--no-config",
2932
"--no-editorconfig",
33+
"--object-wrap",
34+
"collapse",
3035
"--write",
3136
];
3237

@@ -75,6 +80,7 @@ const enum HookName {
7580
Autoflake = "autoflake",
7681
Black = "Black",
7782
ClangFormat = "ClangFormat",
83+
EsLint = "ESLint",
7884
Gofmt = "gofmt",
7985
GoogleJavaFormat = "google-java-format",
8086
Isort = "isort",
@@ -184,6 +190,25 @@ const HOOKS: Record<HookName, LockableHook> = {
184190
include: /\.(cpp|proto$)/,
185191
runAfter: [HookName.Sed],
186192
}),
193+
[HookName.EsLint]: createLockableHook({
194+
action: async sources => {
195+
try {
196+
await run(
197+
"eslint",
198+
"--fix",
199+
"--config",
200+
"/eslint.config.js",
201+
...sources,
202+
);
203+
} catch {
204+
// We swallow nonzero exit codes because we care only about autofixable
205+
// errors (which should now be fixed), not about non-autofixable errors
206+
}
207+
},
208+
exclude: MINIFIED_JS_REGEX,
209+
include: /\.[jt]sx?$/,
210+
runAfter: [HookName.Sed],
211+
}),
187212
[HookName.Gofmt]: createLockableHook({
188213
action: sources => run("/gofmt", "-s", "-w", ...sources),
189214
include: /\.go$/,
@@ -238,9 +263,9 @@ const HOOKS: Record<HookName, LockableHook> = {
238263
"es5",
239264
...sources,
240265
),
241-
exclude: /\b(compressed|custom|min|minified|pack|prod|production)\b/,
266+
exclude: MINIFIED_JS_REGEX,
242267
include: /\.jsx?$/,
243-
runAfter: [HookName.Sed],
268+
runAfter: [HookName.Sed, HookName.EsLint],
244269
}),
245270
[HookName.PrettierNonJs]: createLockableHook({
246271
action: sources =>
@@ -253,7 +278,7 @@ const HOOKS: Record<HookName, LockableHook> = {
253278
...sources,
254279
),
255280
include: /\.(css|html?|markdown|md|scss|tsx?|xml|ya?ml)$/,
256-
runAfter: [HookName.Sed, HookName.Xsltproc],
281+
runAfter: [HookName.Sed, HookName.Xsltproc, HookName.EsLint],
257282
}),
258283
[HookName.Ruff]: createLockableHook({
259284
action: async (sources, args) => {

0 commit comments

Comments
 (0)