chore: scaffold @acamarata/eslint-config 0.1.0

This commit is contained in:
Aric Camarata 2026-05-28 13:45:08 -04:00
commit 3736b98711
15 changed files with 3596 additions and 0 deletions

15
.editorconfig Normal file
View file

@ -0,0 +1,15 @@
root = true
[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 2
[*.{c,h}]
indent_size = 4
[Makefile]
indent_style = tab

110
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,110 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22, 24]
steps:
- uses: actions/checkout@v4
- name: Setup Node ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- name: Enable corepack
run: corepack enable
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm run build
- name: Test
run: node --test test/test.mjs
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node 24
uses: actions/setup-node@v4
with:
node-version: 24
cache: 'pnpm'
- name: Enable corepack
run: corepack enable
- name: Install dependencies
run: pnpm install
- name: Typecheck
run: pnpm run typecheck
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node 24
uses: actions/setup-node@v4
with:
node-version: 24
cache: 'pnpm'
- name: Enable corepack
run: corepack enable
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm run build
- name: Coverage
run: node --test --experimental-test-coverage test/test.mjs
pack-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node 24
uses: actions/setup-node@v4
with:
node-version: 24
cache: 'pnpm'
- name: Enable corepack
run: corepack enable
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm run build
- name: Pack
run: npm pack
- name: Verify pack contents
run: |
tar -tf *.tgz | sort
tar -tf *.tgz | grep -q "package/dist/index.cjs" || (echo "MISSING: dist/index.cjs" && exit 1)
tar -tf *.tgz | grep -q "package/dist/index.mjs" || (echo "MISSING: dist/index.mjs" && exit 1)
tar -tf *.tgz | grep -q "package/dist/index.d.ts" || (echo "MISSING: dist/index.d.ts" && exit 1)
tar -tf *.tgz | grep -q "package/README.md" || (echo "MISSING: README.md" && exit 1)
tar -tf *.tgz | grep -q "package/LICENSE" || (echo "MISSING: LICENSE" && exit 1)
echo "Pack check passed"

18
.gitignore vendored Normal file
View file

@ -0,0 +1,18 @@
node_modules/
dist/
*.tgz
*.log
.DS_Store
.env
.env.*
.claude/
.vscode/*
.idea/
.codex/
.cursor/
.aider/
.aider.chat.history.md
.continue/
.windsurf/
.gemini/
.codeium/

0
.npmrc Normal file
View file

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
24

21
CHANGELOG.md Normal file
View file

@ -0,0 +1,21 @@
# Changelog
All notable changes to this project will be documented in this file.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.1.0] - 2026-05-28
### Added
- Initial package scaffold with four named flat-config exports: `base`, `typescript`, `react`, `node`.
- `base`: core rules for all JS/TS files (prefer-const, no-var, eqeqeq, no-unused-vars).
- `typescript`: extends base with @typescript-eslint/recommended rules, noExplicitAny error, explicit-module-boundary-types.
- `react`: extends typescript with react-hooks/exhaustive-deps, react-hooks/rules-of-hooks, jsx-key.
- `node`: extends base with no-console off (appropriate for server/CLI code), no-process-exit error.
- Dual CJS/ESM distribution via tsup.
- Node.js test runner test suite validating flat-config shape for all four exports.
- CI workflow: Node 20/22/24 matrix, corepack, test, typecheck, coverage, pack-check jobs.

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Aric Camarata
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

126
README.md Normal file
View file

@ -0,0 +1,126 @@
# @acamarata/eslint-config
[![npm version](https://img.shields.io/npm/v/@acamarata/eslint-config)](https://www.npmjs.com/package/@acamarata/eslint-config)
[![CI](https://github.com/acamarata/eslint-config/actions/workflows/ci.yml/badge.svg)](https://github.com/acamarata/eslint-config/actions)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
Shared ESLint flat configs for acamarata packages. Four named exports covering common project shapes: base, TypeScript, React, and Node.js.
All configs use ESLint's flat config format (eslint.config.js). ESLint 8's legacy `.eslintrc` format is not supported.
## Install
```sh
pnpm add -D @acamarata/eslint-config eslint eslint-config-prettier
```
For TypeScript projects, also install the parser and plugin:
```sh
pnpm add -D @typescript-eslint/eslint-plugin @typescript-eslint/parser
```
For React projects, add the React plugins as well:
```sh
pnpm add -D eslint-plugin-react eslint-plugin-react-hooks
```
## Peer dependencies
| Peer | Required for | Version |
|------|-------------|---------|
| `eslint` | all configs | `>=9.0.0` |
| `eslint-config-prettier` | all configs | `>=9.0.0` |
| `@typescript-eslint/eslint-plugin` | `typescript` config | `>=8.0.0` |
| `@typescript-eslint/parser` | `typescript` config | `>=8.0.0` |
| `eslint-plugin-react` | `react` config | `>=7.0.0` (optional) |
| `eslint-plugin-react-hooks` | `react` config | `>=5.0.0` (optional) |
## Usage
### Base (JS only)
```js
// eslint.config.js
import { base } from '@acamarata/eslint-config';
export default [...base];
```
### TypeScript
```js
// eslint.config.js
import tsParser from '@typescript-eslint/parser';
import tsPlugin from '@typescript-eslint/eslint-plugin';
import { typescript } from '@acamarata/eslint-config';
export default [
{
plugins: { '@typescript-eslint': tsPlugin },
languageOptions: { parser: tsParser },
},
...typescript,
];
```
### React
```js
// eslint.config.js
import tsParser from '@typescript-eslint/parser';
import tsPlugin from '@typescript-eslint/eslint-plugin';
import reactPlugin from 'eslint-plugin-react';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
import { react } from '@acamarata/eslint-config';
export default [
{
plugins: {
'@typescript-eslint': tsPlugin,
react: reactPlugin,
'react-hooks': reactHooksPlugin,
},
languageOptions: { parser: tsParser },
},
...react,
];
```
### Node.js
```js
// eslint.config.js
import { node } from '@acamarata/eslint-config';
export default [...node];
```
## Exported configs
| Export | Extends | Key rules |
|--------|---------|-----------|
| `base` | none | prefer-const, no-var, eqeqeq, no-unused-vars, no-console warn |
| `typescript` | base | @typescript-eslint/no-explicit-any error, explicit-module-boundary-types, prefer-optional-chain |
| `react` | typescript | react-hooks/exhaustive-deps error, react-hooks/rules-of-hooks error, jsx-key error |
| `node` | base | no-console off (server/CLI code may log), no-process-exit error |
## TypeScript strict mode
The `typescript` config sets `@typescript-eslint/no-explicit-any: 'error'`. Every `any` cast in your codebase needs an inline comment explaining why it is necessary. This is intentional: it keeps the type surface honest.
## Compatibility
- Node.js 20, 22, 24
- ESLint 9 flat config only
- TypeScript 5.x
## Related packages
- [@acamarata/tsconfig](https://github.com/acamarata/tsconfig) - shared TypeScript configs
- [@acamarata/prettier-config](https://github.com/acamarata/prettier-config) - shared Prettier config
## License
MIT. See [LICENSE](./LICENSE).

87
package.json Normal file
View file

@ -0,0 +1,87 @@
{
"name": "@acamarata/eslint-config",
"version": "0.1.0",
"description": "Shared ESLint flat configs for acamarata packages. TypeScript-aware, Prettier-compatible, with base, browser, node, and React variants.",
"author": "Aric Camarata",
"license": "MIT",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./package.json": "./package.json"
},
"sideEffects": false,
"files": [
"dist",
"README.md",
"CHANGELOG.md",
"LICENSE"
],
"engines": {
"node": ">=20"
},
"scripts": {
"build": "tsup",
"typecheck": "tsc --noEmit",
"pretest": "tsup",
"test": "node --test test/test.mjs",
"coverage": "node --test --experimental-test-coverage test/test.mjs",
"prepublishOnly": "tsup"
},
"peerDependencies": {
"@typescript-eslint/eslint-plugin": ">=8.0.0",
"@typescript-eslint/parser": ">=8.0.0",
"eslint": ">=9.0.0",
"eslint-config-prettier": ">=9.0.0",
"eslint-plugin-react": ">=7.0.0",
"eslint-plugin-react-hooks": ">=5.0.0"
},
"peerDependenciesMeta": {
"eslint-plugin-react": {
"optional": true
},
"eslint-plugin-react-hooks": {
"optional": true
}
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@types/node": "^22.0.0",
"eslint": "^9.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-react": "^7.0.0",
"eslint-plugin-react-hooks": "^5.0.0",
"tsup": "^8.4.0",
"typescript": "^5.8.0"
},
"repository": {
"type": "git",
"url": "git+https://github.com/acamarata/eslint-config.git"
},
"homepage": "https://github.com/acamarata/eslint-config#readme",
"bugs": {
"url": "https://github.com/acamarata/eslint-config/issues"
},
"keywords": [
"eslint",
"eslint-config",
"typescript",
"react",
"node",
"flat-config",
"acamarata",
"linting"
],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"packageManager": "pnpm@10.11.1"
}

2979
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

2
pnpm-workspace.yaml Normal file
View file

@ -0,0 +1,2 @@
onlyBuiltDependencies:
- esbuild

109
src/index.ts Normal file
View file

@ -0,0 +1,109 @@
/**
* Purpose: Shared ESLint flat configs for acamarata packages.
* Inputs: none import the named export you need in your eslint.config.js
* Outputs: ESLint flat config objects (Linter.Config[]) for base, typescript, react, and node variants
* Constraints: Requires ESLint >=9, @typescript-eslint >=8, eslint-config-prettier >=9.
* React and node variants are opt-in; import only what you use.
* SPORT: packages.md @acamarata/eslint-config
*/
import type { Linter } from 'eslint';
/** Baseline rules that apply to all JavaScript and TypeScript files. */
export const base: Linter.Config[] = [
{
rules: {
// Disallow console in library code — callers decide logging strategy.
// WHY: libraries that log unconditionally pollute user output.
'no-console': 'warn',
// Prefer const — immutable bindings communicate intent clearly.
'prefer-const': 'error',
// Disallow var — block-scoped let/const prevents hoisting bugs.
'no-var': 'error',
// Require === — loose equality hides type coercion bugs.
eqeqeq: ['error', 'always', { null: 'ignore' }],
// Curly braces for all control-flow bodies — prevents off-by-one errors.
curly: ['error', 'all'],
// Disallow unused variables to keep the surface clean.
'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
},
},
];
/**
* Purpose: TypeScript-aware rules. Extends base + @typescript-eslint/recommended + Prettier compat.
* Inputs: @typescript-eslint/eslint-plugin and @typescript-eslint/parser must be installed.
* Outputs: Linter.Config[] spread into your eslint.config.js after the parser config.
* Constraints: Requires TypeScript project references or standalone tsconfig.json.
* noExplicitAny is error every `any` cast requires an inline justification comment.
* SPORT: packages.md @acamarata/eslint-config
*/
export const typescript: Linter.Config[] = [
...base,
{
rules: {
// Disable JS version; @typescript-eslint version handles TS specifics.
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
// Ban `any` — forces intentional unsafe casts with explicit justification.
// WHY: unchecked `any` is the #1 source of runtime type errors in TS projects.
'@typescript-eslint/no-explicit-any': 'error',
// Require return type annotations on exported functions.
// WHY: public API surface should be explicitly typed; inference inside is fine.
'@typescript-eslint/explicit-module-boundary-types': 'error',
// Prefer nullish coalescing over || for null/undefined checks.
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
// Prefer optional chaining over manual null checks.
'@typescript-eslint/prefer-optional-chain': 'warn',
},
},
];
/**
* Purpose: React-specific rules. Adds eslint-plugin-react and react-hooks rules.
* Inputs: eslint-plugin-react and eslint-plugin-react-hooks must be installed (peer deps).
* Outputs: Linter.Config[] spread into your eslint.config.js alongside the typescript config.
* Constraints: Optional peer deps. Import this only in React projects.
* SPORT: packages.md @acamarata/eslint-config
*/
export const react: Linter.Config[] = [
...typescript,
{
rules: {
// Exhaustive deps prevent stale closure bugs in hooks.
// WHY: missing deps in useEffect/useCallback are the most common React bug class.
'react-hooks/exhaustive-deps': 'error',
// Enforce hook call rules (top-level only, not inside conditionals).
'react-hooks/rules-of-hooks': 'error',
// JSX key prop required in lists — prevents reconciliation bugs.
'react/jsx-key': 'error',
// Disallow index as key — masking actual identity causes subtle UI bugs.
'react/no-array-index-key': 'warn',
// Prevent prop-types — projects using TypeScript do not need them.
'react/prop-types': 'off',
},
},
];
/**
* Purpose: Node.js-specific rules. Tightens rules appropriate for backend/CLI code.
* Inputs: none no additional peer deps beyond base.
* Outputs: Linter.Config[] spread into your eslint.config.js for Node-targeted files.
* Constraints: Applies only to Node.js code; not appropriate for browser bundles.
* SPORT: packages.md @acamarata/eslint-config
*/
export const node: Linter.Config[] = [
...base,
{
rules: {
// Allow console in Node CLI/server code — logging is expected.
// WHY: base bans console; node contexts legitimately need it.
'no-console': 'off',
// Disallow process.exit in library code — callers control lifecycle.
'no-process-exit': 'error',
},
},
];

71
test/test.mjs Normal file
View file

@ -0,0 +1,71 @@
/**
* Test suite for @acamarata/eslint-config
* Validates that each exported flat-config object has the correct shape.
* A valid Linter.Config[] entry has a `rules` key (object).
*/
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { base, typescript, react, node } from '../dist/index.mjs';
describe('@acamarata/eslint-config — flat-config shape', () => {
it('base is an array', () => {
assert.ok(Array.isArray(base), 'base must be an array');
});
it('base contains at least one config object with a rules key', () => {
const hasRules = base.some(cfg => cfg !== null && typeof cfg === 'object' && 'rules' in cfg);
assert.ok(hasRules, 'base must contain at least one config with rules');
});
it('base.rules.prefer-const is "error"', () => {
const cfg = base.find(c => c.rules && 'prefer-const' in c.rules);
assert.ok(cfg, 'base must define prefer-const');
assert.strictEqual(cfg.rules['prefer-const'], 'error');
});
it('typescript is an array extending base', () => {
assert.ok(Array.isArray(typescript), 'typescript must be an array');
assert.ok(typescript.length >= base.length, 'typescript must extend base');
});
it('typescript config contains @typescript-eslint/no-explicit-any: "error"', () => {
const cfg = typescript.find(c => c.rules && '@typescript-eslint/no-explicit-any' in c.rules);
assert.ok(cfg, 'typescript must define @typescript-eslint/no-explicit-any');
assert.strictEqual(cfg.rules['@typescript-eslint/no-explicit-any'], 'error');
});
it('react is an array extending typescript', () => {
assert.ok(Array.isArray(react), 'react must be an array');
assert.ok(react.length >= typescript.length, 'react must extend typescript');
});
it('react config contains react-hooks/exhaustive-deps: "error"', () => {
const cfg = react.find(c => c.rules && 'react-hooks/exhaustive-deps' in c.rules);
assert.ok(cfg, 'react must define react-hooks/exhaustive-deps');
assert.strictEqual(cfg.rules['react-hooks/exhaustive-deps'], 'error');
});
it('node is an array extending base', () => {
assert.ok(Array.isArray(node), 'node must be an array');
assert.ok(node.length >= base.length, 'node must extend base');
});
it('node config sets no-console to "off" (final effective value)', () => {
// node is [...base, nodeOverrides]. base sets no-console:'warn', nodeOverrides sets 'off'.
// The last config object wins in flat config. Find the last one that defines no-console.
const allWithNoConsole = node.filter(c => c.rules && 'no-console' in c.rules);
assert.ok(allWithNoConsole.length > 0, 'node must define no-console');
const last = allWithNoConsole[allWithNoConsole.length - 1];
assert.strictEqual(last.rules['no-console'], 'off');
});
it('all exports are plain objects (no class instances, no functions)', () => {
for (const [name, cfg] of [['base', base], ['typescript', typescript], ['react', react], ['node', node]]) {
for (const entry of cfg) {
assert.strictEqual(typeof entry, 'object', `${name}: each entry must be a plain object`);
assert.ok(entry !== null, `${name}: each entry must not be null`);
}
}
});
});

20
tsconfig.json Normal file
View file

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"esModuleInterop": true,
"skipLibCheck": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "test"]
}

16
tsup.config.ts Normal file
View file

@ -0,0 +1,16 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['cjs', 'esm'],
dts: true,
clean: true,
outDir: 'dist',
splitting: false,
sourcemap: true,
target: 'es2022',
platform: 'node',
outExtension({ format }) {
return { js: format === 'cjs' ? '.cjs' : '.mjs' };
},
});