offline-first, p2p synced, atproto enabled, feed reader
1import path from 'node:path'
2
3import {includeIgnoreFile} from '@eslint/compat'
4import js from '@eslint/js'
5import json from '@eslint/json'
6import restrictedGlobals from 'confusing-browser-globals'
7import {defineConfig} from 'eslint/config'
8import {importX} from 'eslint-plugin-import-x'
9import prettier from 'eslint-plugin-prettier/recommended'
10import solid from 'eslint-plugin-solid/configs/typescript'
11import globals from 'globals'
12import tseslint from 'typescript-eslint'
13
14const gitignore = path.resolve(import.meta.dirname, '.gitignore')
15
16export default defineConfig(
17 includeIgnoreFile(gitignore, '.gitignore'),
18
19 // all files by default get shared globals
20 {
21 name: 'javascript basics',
22 files: ['**/*.@(js|jsx|ts|tsx)'],
23 extends: [js.configs.recommended],
24
25 languageOptions: {
26 globals: {
27 ...globals.es2024,
28 ...globals['shared-node-browser'],
29 },
30 },
31
32 rules: {
33 // eg, `open` is a global, but probably not really intended that way in our code
34 'no-restricted-globals': ['error', ...restrictedGlobals],
35 'no-unused-vars': ['warn', {varsIgnorePattern: '(?:^_)'}],
36 },
37 },
38
39 {
40 name: 'typescript basics',
41 files: ['**/*.@(ts|tsx)'],
42 extends: [tseslint.configs.strictTypeChecked],
43
44 languageOptions: {
45 parserOptions: {
46 projectService: true,
47 tsconfigRootDir: import.meta.dirname,
48 },
49 },
50 rules: {
51 // allow leading underscore for marking unused vars
52 '@typescript-eslint/no-unused-vars': ['warn', {varsIgnorePattern: '(?:^_)'}],
53
54 // template literals default to calling toString(), but I mean what I say
55 '@typescript-eslint/restrict-template-expressions': 'off',
56
57 // I need to be able to do `while(true)`, come on yall...
58 '@typescript-eslint/no-unnecessary-condition': ['warn', {allowConstantLoopConditions: 'always'}],
59
60 // this breaks when I want to use a type parameter to allow type checking inline arguments
61 // it's "unnecessary type parameters" or an `as` declaration, which I don't like
62 '@typescript-eslint/no-unnecessary-type-parameters': 'off',
63 '@typescript-eslint/no-unnecessary-type-constraint': 'off',
64
65 // the breaks Promise.withResolvers<void>(),
66 // see https://github.com/typescript-eslint/typescript-eslint/issues/8113
67 '@typescript-eslint/no-invalid-void-type': 'off',
68 },
69 },
70
71 // import organization and validation
72 {
73 name: 'import rules',
74 files: ['**/*.@(js|jsx|ts|tsx)'],
75 plugins: {'import-x': importX},
76 rules: {
77 // no file extensions on imports (TypeScript handles resolution)
78 'import-x/extensions': ['error', 'never'],
79
80 // merge duplicate imports from same module
81 'import-x/no-duplicates': 'warn',
82
83 // all imports at top of file
84 'import-x/first': 'error',
85
86 // blank line after import block
87 'import-x/newline-after-import': 'error',
88
89 // prevent importing a module from itself
90 'import-x/no-self-import': 'error',
91
92 // consistent import ordering:
93 // 1. node builtins (node:fs, etc)
94 // 2. external packages (zod, solid-js, etc)
95 // 3. internal aliases (#lib, #realm, #feedline, #app, #spec)
96 // 4. relative imports
97 'import-x/order': [
98 'warn',
99 {
100 groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
101 pathGroups: [
102 {pattern: '#lib/**', group: 'internal', position: 'before'},
103 {pattern: '#realm/**', group: 'internal', position: 'before'},
104 {pattern: '#feedline/**', group: 'internal', position: 'before'},
105 {pattern: '#app/**', group: 'internal', position: 'before'},
106 {pattern: '#spec/**', group: 'internal', position: 'after'},
107 ],
108 pathGroupsExcludedImportTypes: ['builtin'],
109 'newlines-between': 'always',
110 alphabetize: {order: 'asc', caseInsensitive: true},
111 },
112 ],
113
114 // detect circular dependencies (can slow down linting on large codebases)
115 // set to warn since cycles in realm/feedline could cause subtle sync bugs
116 'import-x/no-cycle': ['warn', {maxDepth: 4}],
117
118 // enforce zod/mini over zod or zod/v4 for bundle size
119 'no-restricted-imports': [
120 'error',
121 {
122 paths: [
123 {name: 'zod', message: "Use 'zod/mini' instead for smaller bundle size."},
124 {name: 'zod/v4', message: "Use 'zod/mini' instead for smaller bundle size."},
125 ],
126 },
127 ],
128 },
129 },
130
131 {
132 name: 'node files',
133 files: ['src/server/**/*.@(js|jsx|ts|tsx)', 'src/cmd/**/*.@(js|jsx|ts|tsx)'],
134 languageOptions: {
135 globals: {
136 ...globals.es2024,
137 ...globals.node,
138 },
139 },
140 },
141
142 {
143 name: 'client files',
144 files: ['src/**/client/**/*.@(js|ts)', 'src/feedline/**/*.@(js|ts)', 'src/**/*.@(jsx|tsx)'],
145 ...solid,
146 languageOptions: {
147 globals: {
148 ...globals.es2024,
149 ...globals.browser,
150 },
151 },
152 rules: {
153 // our custom reactive query helpers
154 'solid/reactivity': ['warn', {customReactiveFunctions: ['makeSignalQuery', 'makeStoreQuery']}],
155 },
156 },
157
158 // tests don't have jsdoc requirements, and looser any/null assertion restrictions
159 {
160 files: ['src/**/*.spec.{js,jsx,ts,tsx}', 'src/spec/**/*.{js,ts,jsx,tsx}'],
161 rules: {
162 '@typescript-eslint/no-explicit-any': 'off',
163 '@typescript-eslint/no-unsafe-assignment': 'off',
164 '@typescript-eslint/no-unsafe-argument': 'off',
165 '@typescript-eslint/no-unsafe-member-access': 'off',
166 '@typescript-eslint/no-unsafe-return': 'off',
167 '@typescript-eslint/no-non-null-assertion': 'off',
168 },
169 },
170
171 // json (with comments in some files)
172 {
173 files: ['**/*.json'],
174 ignores: ['package-lock.json'],
175
176 plugins: {json},
177 language: 'json/json',
178 extends: [json.configs.recommended],
179 },
180 {
181 files: ['tsconfig.json'],
182 language: 'json/jsonc',
183 },
184
185 // prettier last, so it can turn everything off
186 prettier,
187)