Skip to content
Merged
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
22 changes: 22 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Lint

on:
push:
branches:
- main
pull_request:
workflow_dispatch:

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.14"

- run: bun install --frozen-lockfile

- run: bun run lint
173 changes: 167 additions & 6 deletions bun.lock

Large diffs are not rendered by default.

68 changes: 68 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import stylistic from '@stylistic/eslint-plugin';
import tseslint from 'typescript-eslint';

export default [
{
ignores: [ 'dist/**', 'node_modules/**', 'bun.lock' ]
},
...tseslint.configs.recommended,
{
plugins: {
'@stylistic': stylistic
},
rules: {
'@stylistic/array-bracket-newline': [ 'error', { multiline: true } ],
'@stylistic/array-bracket-spacing': [ 'error', 'always' ],
'@stylistic/arrow-spacing': 'error',
'@stylistic/comma-spacing': [ 'error', { before: false, after: true } ],
'@stylistic/dot-location': [ 'error', 'property' ],
'@stylistic/eol-last': [ 'error', 'always' ],
'@stylistic/indent': [
'error', 2, {
SwitchCase: 1,
MemberExpression: 1,
ArrayExpression: 1,
ObjectExpression: 1,
ImportDeclaration: 1,
flatTernaryExpressions: false
}
],
'@stylistic/key-spacing': [ 'error', { afterColon: true } ],
'@stylistic/lines-between-class-members': [ 'error', 'always' ],
'@stylistic/no-mixed-spaces-and-tabs': 'error',
'@stylistic/no-multi-spaces': 'error',
'@stylistic/no-multiple-empty-lines': [ 'error', { max: 1 } ],
'@stylistic/no-tabs': 'error',
'@stylistic/no-trailing-spaces': [ 'error' ],
'@stylistic/no-whitespace-before-property': 'error',
'@stylistic/space-before-function-paren': [
'error', {
asyncArrow: 'always',
anonymous: 'never',
named: 'never'
}
],
'@stylistic/space-in-parens': [ 'error', 'always' ],
'@stylistic/space-infix-ops': [ 'error', { int32Hint: false } ],
'@stylistic/template-curly-spacing': [ 'error', 'always' ],
'@stylistic/space-before-blocks': 'error',
'@stylistic/type-annotation-spacing': 'error',
'@stylistic/curly-newline': [ 'error', 'always' ],
'@stylistic/object-curly-spacing': [ 'error', 'always' ],
'@stylistic/operator-linebreak': [ 'error', 'after' ],
'@stylistic/quotes': [ 'error', 'single', { avoidEscape: true } ],
'@typescript-eslint/no-unused-vars': [
'error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
ignoreRestSiblings: true
}
],
'@typescript-eslint/no-explicit-any': 'off',
curly: 'error',
'no-console': [ 'error', { allow: [ 'warn', 'error', 'info' ] } ],
semi: [ 'error', 'always' ]
}
}
];
2 changes: 2 additions & 0 deletions openspec/changes/setup-eslint/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-02
89 changes: 89 additions & 0 deletions openspec/changes/setup-eslint/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
## Context

Fetchify is a small TypeScript library (Bun-based build, `bun test` for
tests, Changesets for releases). There is no linting today. The user supplied
a ready-made ESLint flat config built on `typescript-eslint` and
`@stylistic/eslint-plugin`, and asked for ESLint 10 plus a sensible
`ignores` list. The repo has no JSX/React code and no `eslint-plugin-prettier`
dependency, so parts of the supplied config don't apply as-is.

## Goals / Non-Goals

**Goals:**
- Install ESLint 10 (flat config only; no `.eslintrc*`).
- Reproduce the supplied stylistic and TypeScript rule set as closely as
possible for the parts that are relevant to this codebase.
- Provide a correct `ignores` array for this repo's generated/vendored paths.
- Provide a `bun run lint` script so the config is actually usable.
- Run lint automatically on every pull request via GitHub Actions, mirroring
the existing `test.yml` trigger shape, with a manual `workflow_dispatch`
trigger for testing the workflow itself.

**Non-Goals:**
- Adding Prettier or `eslint-plugin-prettier` — not present in this repo, and
out of scope for this change.
- Adding React/JSX tooling — no JSX exists in `src/**`.
- Auto-fixing existing source to satisfy the new rules — this change only
adds the tool; a separate pass can fix violations if any turn up.

## Decisions

1. **Drop all `@stylistic/jsx-*` rules.**
Alternative considered: keep them inert since unused rules on non-JSX
files are harmless. Rejected because `tsconfig.json` sets
`"jsx": "react-jsx"` as a leftover default, not because JSX is in use —
grepping `src/**` confirms no `.tsx` files or JSX syntax. Keeping
JSX-specific rules would misrepresent the project's actual scope and add
config the team has to reason about for no benefit. If JSX is ever added,
these can be reintroduced then.

2. **Drop `'prettier/prettier': 'off'`.**
ESLint's flat config will throw at lint time if a rule ID references a
plugin that isn't registered (`prettier` isn't in `plugins`). Since this
repo has no Prettier setup, keeping this line would break `eslint.config.js`
outright. Simplest fix is to omit it entirely rather than adding a Prettier
dependency just to satisfy one disabled rule.

3. **`ignores`: `["dist/**", "node_modules/**", "bun.lock"]`.**
`dist/` is build output (`bun run build`), `node_modules/` is standard,
and `bun.lock` is a lockfile, not source. `coverage/` is not currently
produced by `bun test` in this repo, so it's omitted rather than
speculatively added; it can be appended later if coverage output starts
landing at the repo root.

4. **Keep the full `@typescript-eslint` and non-JSX `@stylistic` rule list
as supplied**, including `no-console` allowing `warn`/`error`/`info` and
`@typescript-eslint/no-explicit-any: off`. These are direct user
preferences from the supplied config; no reason to second-guess them.

5. **Add `eslint.config.js` at repo root** (flat config, ESM, matches
`"type": "module"` in `package.json`), rather than `.mjs`/`.cjs` — consistent
with how the rest of the repo is authored (`src/**` is all ESM `.ts`).

6. **New `lint` script**: `"lint": "eslint ."`. Simple, matches the
`bun run <script>` convention already used for `build`/`changeset`/`version`.

7. **Add `.github/workflows/lint.yml` mirroring `test.yml`'s trigger shape
exactly** (`push` to `main`, `pull_request`, `workflow_dispatch`), as a
separate workflow file rather than a second job in `test.yml`.
Alternative considered: add a `lint` job to the existing `test` workflow.
Rejected in favor of a separate file so lint and test results show as
distinct checks on a PR and either can be re-run independently; this also
keeps the diff to `test.yml` at zero. The job installs Bun the same way
(`oven-sh/setup-bun@v2`, same pinned version), runs
`bun install --frozen-lockfile`, then `bun run lint`.

## Risks / Trade-offs

- [Existing source may fail new rules once ESLint actually runs] →
Mitigation: tasks include running `bun run lint` after setup and fixing (or
explicitly deferring) any violations found, so the change isn't merged with
a broken lint script.
- [ESLint 10 + `typescript-eslint` + `@stylistic/eslint-plugin` version
compatibility is unverified at proposal time] → Mitigation: pin to the
latest compatible majors during install and confirm `eslint.config.js`
loads without error (`bunx eslint --version` / a no-op lint run) as a task.
- [Team may later add JSX/React] → Mitigation: the dropped `jsx-*` rules are
documented in this design and the source rule set is preserved in the
proposal, so re-adding them is a small, well-understood change.

52 changes: 52 additions & 0 deletions openspec/changes/setup-eslint/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
## Why

Fetchify has no linting in place today, so style drift and unsafe TypeScript
patterns (unused vars, implicit `any`, missing semicolons, etc.) can only be
caught in code review. Adding ESLint 10 with `typescript-eslint` and
`@stylistic/eslint-plugin` enforces a consistent style and catches common
mistakes automatically, both locally and in CI.

## What Changes

- Add ESLint 10 as a dev dependency, along with `typescript-eslint` and
`@stylistic/eslint-plugin`.
- Add a flat config `eslint.config.js` at the repo root based on
`tseslint.configs.recommended` plus the stylistic and general rules
supplied by the user.
- Drop the JSX-specific `@stylistic/jsx-*` rules from the supplied rule set:
fetchify is a plain TypeScript library with no `.tsx`/React code (confirmed
by inspecting `src/**` and `tsconfig.json`'s `allowJs`/`jsx` settings are
vestigial defaults, not in active use), so those rules would never fire and
only add noise.
- Drop `prettier/prettier: 'off'` from the supplied rule set: there is no
`eslint-plugin-prettier` in this project, so referencing that rule id would
make the config invalid. Prettier is not otherwise part of this change.
- Set `ignores` to cover generated/vendored output: `dist/**`,
`node_modules/**`, `bun.lock`, and coverage output if introduced later.
- Add a `bun run lint` script (`eslint .`) to `package.json`.
- Add a `.github/workflows/lint.yml` GitHub Actions workflow that runs
`bun run lint` on every pull request and can also be triggered manually
via `workflow_dispatch`, mirroring the existing `test.yml` trigger shape.

## Capabilities

### New Capabilities
- `lint-tooling`: Static analysis/style enforcement for the codebase via
ESLint 10, including the flat config, rule set, and the `lint` script used
to run it locally and in CI.

### Modified Capabilities
- none

## Impact

- **Affected code**: repo root (new `eslint.config.js`), `package.json`
(new dependencies + `lint` script), `.github/workflows/lint.yml` (new).
No changes to `src/**` behavior.
- **Dependencies added**: `eslint@^10`, `typescript-eslint`,
`@stylistic/eslint-plugin` (dev dependencies).
- **CI**: a new `lint` workflow runs `bun run lint` on every pull request
and supports manual runs via `workflow_dispatch`, alongside the existing
`test.yml` workflow.
- **Changesets**: this is internal tooling with no user-facing effect on the
published package, so no changeset entry is required per `CLAUDE.md`.
69 changes: 69 additions & 0 deletions openspec/changes/setup-eslint/specs/lint-tooling/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
## ADDED Requirements

### Requirement: ESLint flat config for the project
The repository SHALL provide a root-level `eslint.config.js` using ESLint's
flat config format, built on `typescript-eslint`'s recommended config and
`@stylistic/eslint-plugin`, that lints all TypeScript source files.

#### Scenario: Linting a compliant file passes
- **WHEN** `bun run lint` is executed against a TypeScript file that follows
the configured style and type-safety rules
- **THEN** ESLint reports no errors for that file

#### Scenario: Linting a non-compliant file fails
- **WHEN** `bun run lint` is executed against a TypeScript file containing a
violation of a configured rule (e.g. a missing semicolon, double quotes
instead of single quotes, or an unused variable not prefixed with `_`)
- **THEN** ESLint reports an error for that violation and exits non-zero

### Requirement: Generated and vendored paths are excluded from linting
The ESLint config SHALL ignore build output, dependencies, and lockfiles so
they are never linted.

#### Scenario: Build output is ignored
- **WHEN** `bun run lint` is executed with a `dist/**` directory present
- **THEN** ESLint does not report on any file under `dist/`

#### Scenario: Dependencies and lockfile are ignored
- **WHEN** `bun run lint` is executed
- **THEN** ESLint does not report on any file under `node_modules/` or on
`bun.lock`

### Requirement: Lint script is available via the package manager
The `package.json` SHALL expose a `lint` script runnable via `bun run lint`
that invokes ESLint over the project.

#### Scenario: Running lint via bun
- **WHEN** a developer runs `bun run lint`
- **THEN** ESLint executes using `eslint.config.js` and reports results for
the project's source files

### Requirement: Unused variables are allowed via underscore prefix
The `@typescript-eslint/no-unused-vars` rule SHALL permit unused function
arguments, variables, caught errors, and destructured rest siblings when
their name is prefixed with `_`.

#### Scenario: Underscore-prefixed unused argument is allowed
- **WHEN** a function argument named `_req` is declared but never used
within the function body
- **THEN** ESLint does not report a `no-unused-vars` violation for `_req`

#### Scenario: Non-prefixed unused variable is flagged
- **WHEN** a local variable is declared and never used, and its name does
not start with `_`
- **THEN** ESLint reports a `no-unused-vars` error

### Requirement: Lint runs in CI on every pull request
The repository SHALL provide a GitHub Actions workflow that runs `bun run
lint` on every pull request and that can also be triggered manually.

#### Scenario: Lint runs automatically on a pull request
- **WHEN** a pull request is opened or updated against the repository
- **THEN** the lint workflow runs and reports a failed check if `bun run
lint` exits non-zero

#### Scenario: Lint can be triggered manually
- **WHEN** a maintainer manually triggers the lint workflow via
`workflow_dispatch` (e.g. from the GitHub Actions UI)
- **THEN** the workflow runs `bun run lint` the same way it would on a
pull request
27 changes: 27 additions & 0 deletions openspec/changes/setup-eslint/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## 1. Install dependencies

- [x] 1.1 Add `eslint@^10`, `typescript-eslint`, and `@stylistic/eslint-plugin` as dev dependencies via `bun add -d`.
- [x] 1.2 Confirm installed versions of `eslint`, `typescript-eslint`, and `@stylistic/eslint-plugin` are mutually compatible (check each package's peer dependency ranges).

## 2. Add flat config

- [x] 2.1 Create `eslint.config.js` at the repo root using `tseslint.configs.recommended` as the base.
- [x] 2.2 Add the `ignores` entry: `["dist/**", "node_modules/**", "bun.lock"]`.
- [x] 2.3 Register the `@stylistic` plugin and port over the supplied stylistic rules, excluding all `@stylistic/jsx-*` rules (no JSX in this repo).
- [x] 2.4 Port over the supplied `@typescript-eslint` and general rules (`no-unused-vars`, `no-explicit-any: off`, `curly`, `no-console`, `semi`), omitting `'prettier/prettier': 'off'` since no `eslint-plugin-prettier` is installed.

## 3. Wire up the script

- [x] 3.1 Add `"lint": "eslint ."` to `package.json` scripts.

## 4. Verify

- [x] 4.1 Run `bunx eslint --version` (or equivalent) to confirm ESLint 10 is resolved and `eslint.config.js` loads without error.
- [x] 4.2 Run `bun run lint` against the existing `src/**` and fix or explicitly document any resulting violations.
- [x] 4.3 Spot-check the `ignores` list by confirming `bun run lint` does not report on files under `dist/`.

## 5. Add CI workflow

- [x] 5.1 Create `.github/workflows/lint.yml`, mirroring `test.yml`'s structure and pinned `bun-version`, with `on: push (main) / pull_request / workflow_dispatch` and a job that runs `bun install --frozen-lockfile` then `bun run lint`.
- [x] 5.2 Open a test PR (or push to a branch) to confirm the lint workflow triggers automatically and reports status on the PR.
- [x] 5.3 Manually trigger the workflow via `workflow_dispatch` (GitHub Actions UI or `gh workflow run lint.yml`) to confirm it runs on demand.
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@
"scripts": {
"build": "bun build ./src/index.ts ./src/cache/memory.ts --outdir dist --target browser --format esm && tsc -p tsconfig.build.json",
"changeset": "changeset",
"version": "changeset version"
"version": "changeset version",
"lint": "eslint ."
},
"devDependencies": {
"@changesets/cli": "^2.31.1",
"@types/bun": "latest"
"@stylistic/eslint-plugin": "^5.10.0",
"@types/bun": "latest",
"eslint": "^10",
"typescript-eslint": "^8.65.0"
},
"peerDependencies": {
"typescript": "^5"
Expand Down
Loading
Loading