Skip to content

Commit 0ee31a9

Browse files
committed
chore: add build and test configuration
Configure TypeScript, Vite, ESLint, and Vitest: - TypeScript 5.9 with strict mode and ES2022 target - Vite build for ES/CJS/UMD library outputs - ESLint with typescript-eslint and strict rules - Vitest for testing with v8 coverage
1 parent 396c747 commit 0ee31a9

4 files changed

Lines changed: 559 additions & 0 deletions

File tree

eslint.config.ts

Lines changed: 357 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
1+
import js from '@eslint/js';
2+
import tseslint from 'typescript-eslint';
3+
import importPlugin from 'eslint-plugin-import';
4+
import nodePlugin from 'eslint-plugin-n';
5+
import promisePlugin from 'eslint-plugin-promise';
6+
import regexpPlugin from 'eslint-plugin-regexp';
7+
import simpleImportSort from 'eslint-plugin-simple-import-sort';
8+
import preferArrowFunctions from 'eslint-plugin-prefer-arrow-functions';
9+
import noOnlyTests from 'eslint-plugin-no-only-tests';
10+
import jsdoc from 'eslint-plugin-jsdoc';
11+
import unicornPlugin from 'eslint-plugin-unicorn';
12+
import sonarjsPlugin from 'eslint-plugin-sonarjs';
13+
import vitestPlugin from '@vitest/eslint-plugin';
14+
import eslintCommentsPlugin from '@eslint-community/eslint-plugin-eslint-comments';
15+
import stylisticPlugin from '@stylistic/eslint-plugin';
16+
import unusedImports from 'eslint-plugin-unused-imports';
17+
18+
/**
19+
* Custom rule to ban emoji characters in source code.
20+
*
21+
* Unicode emoji matching approaches compared:
22+
*
23+
* - `\p{Emoji}` - Too broad: matches #, *, digits 0-9, which appear in normal code
24+
* - `\p{Emoji_Presentation}` - Too narrow: misses ❤, ⚠, ☀, ✉ and other common symbols
25+
* - `\p{Extended_Pictographic}` - Best balance: catches visual emoji without false positives
26+
*
27+
* Extended_Pictographic matches: 😀 🎉 🔥 ❤ ⬆ ⬇ ✔ ⚠ ℹ ☀ ❄ ✉ ⭐ © ® ™
28+
* Extended_Pictographic avoids: # * 0 1 2 (no false positives on code characters)
29+
*
30+
* Note: © ® ™ are matched, which may appear in license headers. If needed,
31+
* add file-level eslint-disable comments for license files.
32+
*
33+
* @see https://www.stefanjudis.com/snippets/how-to-detect-emojis-in-javascript-strings/
34+
* @see https://unicode.org/reports/tr51/#Extended_Pictographic
35+
*/
36+
const noEmoji = {
37+
meta: {
38+
type: 'problem',
39+
docs: {
40+
description: 'Disallow emoji characters in source code',
41+
},
42+
messages: {
43+
noEmoji: 'Emoji characters are not allowed in source code. Found: "{{emoji}}"',
44+
},
45+
schema: [],
46+
},
47+
create(context) {
48+
// Unicode Extended_Pictographic property - catches emoji without false positives on #, *, digits
49+
const emojiPattern = /\p{Extended_Pictographic}/gu;
50+
51+
const checkForEmoji = (node, value) => {
52+
const matches = value.match(emojiPattern);
53+
if (matches) {
54+
context.report({
55+
node,
56+
messageId: 'noEmoji',
57+
data: { emoji: matches.join(', ') },
58+
});
59+
}
60+
};
61+
62+
return {
63+
Literal(node) {
64+
if (typeof node.value === 'string') {
65+
checkForEmoji(node, node.value);
66+
}
67+
},
68+
TemplateElement(node) {
69+
checkForEmoji(node, node.value.raw);
70+
},
71+
};
72+
},
73+
};
74+
75+
// Custom rule to enforce test file naming pattern
76+
const enforceTestFileTypeNaming = {
77+
meta: {
78+
type: 'problem',
79+
docs: {
80+
description: 'Enforce test files to use type suffix (unit/integration/component)',
81+
},
82+
messages: {
83+
invalidTestFileName:
84+
'Test files must be named with a type suffix: {{name}}.unit.test.ts, {{name}}.integration.test.ts, or {{name}}.component.test.ts. Found: {{fileName}}',
85+
},
86+
schema: [],
87+
},
88+
create(context) {
89+
const fileName = context.filename;
90+
const testNameRegex = /^(.+)\.(unit|integration|component)\.test\.ts$/;
91+
92+
if (fileName.endsWith('.test.ts') && !testNameRegex.test(fileName)) {
93+
const name = fileName.replace(/\.test\.ts$/, '');
94+
context.report({
95+
messageId: 'invalidTestFileName',
96+
data: {
97+
fileName: fileName,
98+
name: name,
99+
},
100+
loc: { line: 0, column: 0 },
101+
});
102+
}
103+
104+
return {};
105+
},
106+
};
107+
108+
export default tseslint.config(
109+
{
110+
ignores: [
111+
'dist/**',
112+
'coverage/**',
113+
'node_modules/**',
114+
'*.config.ts',
115+
'**/*.d.ts',
116+
],
117+
},
118+
{
119+
files: ['**/*.ts'],
120+
extends: [js.configs.recommended, ...tseslint.configs.recommendedTypeChecked],
121+
languageOptions: {
122+
parserOptions: {
123+
projectService: true,
124+
tsconfigRootDir: import.meta.dirname,
125+
},
126+
},
127+
plugins: {
128+
'@typescript-eslint': tseslint.plugin,
129+
import: importPlugin,
130+
n: nodePlugin,
131+
promise: promisePlugin,
132+
regexp: regexpPlugin,
133+
'simple-import-sort': simpleImportSort,
134+
'prefer-arrow-functions': preferArrowFunctions,
135+
jsdoc: jsdoc,
136+
unicorn: unicornPlugin,
137+
sonarjs: sonarjsPlugin,
138+
'custom-rules': {
139+
rules: {
140+
'no-emoji': noEmoji,
141+
},
142+
},
143+
'@eslint-community/eslint-comments': eslintCommentsPlugin,
144+
'@stylistic': stylisticPlugin,
145+
'unused-imports': unusedImports,
146+
},
147+
rules: {
148+
// Custom rules
149+
'custom-rules/no-emoji': 'error',
150+
151+
// Stylistic rules - tabs and double quotes
152+
'@stylistic/indent': ['error', 'tab'],
153+
'@stylistic/quotes': ['error', 'double', { avoidEscape: true }],
154+
'@stylistic/no-tabs': 'off', // Allow tabs since we're using them for indentation
155+
156+
/**
157+
* Ban eslint-disable and similar comments in production code.
158+
* These rules are disabled for test files where they may be needed.
159+
*
160+
* Banned comments:
161+
* - eslint-disable, eslint-disable-line, eslint-disable-next-line
162+
* - @ts-ignore, @ts-expect-error, @ts-nocheck
163+
*
164+
* If you need to disable a rule, fix the underlying issue instead.
165+
*/
166+
'@eslint-community/eslint-comments/no-unlimited-disable': 'error',
167+
'@eslint-community/eslint-comments/no-use': [
168+
'error',
169+
{
170+
allow: [], // disallow all eslint-disable comments in production code
171+
},
172+
],
173+
174+
// TypeScript rules - use unused-imports plugin for auto-fixing
175+
'@typescript-eslint/no-unused-vars': 'off', // Replaced by unused-imports/no-unused-vars
176+
'unused-imports/no-unused-imports': 'error', // Auto-fix unused imports
177+
'unused-imports/no-unused-vars': [
178+
'error',
179+
{
180+
vars: 'all',
181+
varsIgnorePattern: '^_',
182+
args: 'after-used',
183+
argsIgnorePattern: '^_',
184+
},
185+
],
186+
'@typescript-eslint/no-explicit-any': 'error',
187+
'@typescript-eslint/no-non-null-assertion': 'error',
188+
// Ban @ts-ignore, @ts-expect-error, @ts-nocheck (part of disable comment ban above)
189+
'@typescript-eslint/ban-ts-comment': [
190+
'error',
191+
{
192+
'ts-expect-error': true,
193+
'ts-ignore': true,
194+
'ts-nocheck': true,
195+
'ts-check': false, // allow @ts-check
196+
},
197+
],
198+
// Type-aware rules (require projectService)
199+
'@typescript-eslint/no-floating-promises': 'error',
200+
'@typescript-eslint/no-misused-promises': 'error',
201+
'@typescript-eslint/await-thenable': 'error',
202+
'@typescript-eslint/require-await': 'warn', // Async without await is often intentional for interface compliance
203+
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
204+
'@typescript-eslint/no-unnecessary-condition': 'warn', // Sometimes false positives with complex types
205+
'@typescript-eslint/prefer-nullish-coalescing': 'warn', // Style preference, can introduce subtle bugs with 0/''
206+
'@typescript-eslint/prefer-optional-chain': 'error',
207+
'@typescript-eslint/restrict-template-expressions': 'warn',
208+
'@typescript-eslint/no-base-to-string': 'warn',
209+
'@typescript-eslint/no-unsafe-assignment': 'warn', // Type safety, but often false positives
210+
'@typescript-eslint/no-unsafe-return': 'warn', // Type safety, but often false positives
211+
'@typescript-eslint/no-unsafe-argument': 'warn', // Type safety, but often false positives
212+
'prefer-const': 'warn', // Style preference
213+
214+
// Import rules
215+
...importPlugin.configs.recommended.rules,
216+
...importPlugin.configs.typescript.rules,
217+
'import/no-relative-packages': 'error',
218+
'import/no-cycle': 'error',
219+
'import/no-default-export': 'error',
220+
'import/order': 'off', // Using simple-import-sort instead
221+
222+
// Promise rules
223+
...promisePlugin.configs['flat/recommended'].rules,
224+
225+
// Regexp rules
226+
...regexpPlugin.configs['flat/recommended'].rules,
227+
228+
// Simple import sort
229+
'simple-import-sort/imports': 'error',
230+
'simple-import-sort/exports': 'error',
231+
232+
// Prefer arrow functions
233+
'prefer-arrow-functions/prefer-arrow-functions': [
234+
'error',
235+
{
236+
classPropertiesAllowed: false,
237+
disallowPrototype: false,
238+
returnStyle: 'unchanged',
239+
singleReturnOnly: false,
240+
},
241+
],
242+
243+
// JSDoc rules
244+
...jsdoc.configs['flat/recommended-typescript'].rules,
245+
'jsdoc/require-jsdoc': 'off',
246+
'jsdoc/require-description': 'off',
247+
'jsdoc/require-param-description': 'off',
248+
'jsdoc/require-returns-description': 'off',
249+
'jsdoc/require-returns': 'off',
250+
'jsdoc/require-yields': 'off',
251+
'jsdoc/require-yields-type': 'off',
252+
'jsdoc/require-throws-type': 'off',
253+
'jsdoc/check-param-names': 'off',
254+
'jsdoc/escape-inline-tags': 'off',
255+
'jsdoc/tag-lines': 'off',
256+
'jsdoc/check-tag-names': 'warn',
257+
258+
// Unicorn rules (spread recommended, then override noisy ones to warn)
259+
...unicornPlugin.configs['flat/recommended'].rules,
260+
'unicorn/prevent-abbreviations': 'warn',
261+
'unicorn/no-null': 'warn',
262+
'unicorn/no-array-sort': 'warn',
263+
'unicorn/prefer-export-from': 'warn',
264+
'unicorn/prefer-single-call': 'off',
265+
'unicorn/no-array-callback-reference': 'off',
266+
'unicorn/prefer-number-properties': 'warn',
267+
'unicorn/no-new-array': 'warn',
268+
'unicorn/prefer-top-level-await': 'off', // CLI files need different patterns
269+
'unicorn/no-process-exit': 'off', // CLI files need process.exit
270+
'unicorn/no-array-reduce': 'warn',
271+
'unicorn/import-style': 'warn',
272+
'unicorn/no-immediate-mutation': 'warn',
273+
'unicorn/no-array-reverse': 'warn',
274+
'unicorn/consistent-function-scoping': 'warn',
275+
'unicorn/text-encoding-identifier-case': 'warn',
276+
'unicorn/no-useless-switch-case': 'warn',
277+
'unicorn/no-nested-ternary': 'warn',
278+
'unicorn/no-empty-file': 'warn',
279+
'unicorn/no-array-for-each': 'warn',
280+
281+
// SonarJS rules (spread recommended, then override noisy ones to warn)
282+
...sonarjsPlugin.configs.recommended.rules,
283+
'sonarjs/cognitive-complexity': 'warn',
284+
'sonarjs/no-alphabetical-sort': 'warn',
285+
'sonarjs/no-nested-functions': 'warn',
286+
'sonarjs/no-nested-conditional': 'warn',
287+
'sonarjs/redundant-type-aliases': 'warn',
288+
'sonarjs/todo-tag': 'warn',
289+
'sonarjs/updated-loop-counter': 'warn',
290+
'sonarjs/pseudo-random': 'warn',
291+
'sonarjs/unused-import': 'off', // Duplicates @typescript-eslint/no-unused-vars
292+
'sonarjs/no-unused-vars': 'off', // Duplicates @typescript-eslint/no-unused-vars
293+
'sonarjs/different-types-comparison': 'warn',
294+
'sonarjs/prefer-regexp-exec': 'warn',
295+
'sonarjs/no-dead-store': 'warn',
296+
'sonarjs/use-type-alias': 'warn',
297+
'sonarjs/class-name': 'warn',
298+
'sonarjs/slow-regex': 'warn',
299+
'sonarjs/no-all-duplicated-branches': 'warn',
300+
'sonarjs/regex-complexity': 'warn',
301+
'sonarjs/no-unused-collection': 'warn',
302+
'sonarjs/no-nested-template-literals': 'warn',
303+
'sonarjs/no-identical-functions': 'warn',
304+
'sonarjs/no-duplicated-branches': 'warn',
305+
'sonarjs/no-clear-text-protocols': 'warn',
306+
'sonarjs/function-return-type': 'warn',
307+
'sonarjs/arguments-order': 'warn',
308+
309+
// Regexp rules - downgrade some noisy ones
310+
'regexp/no-unused-capturing-group': 'warn',
311+
},
312+
settings: {
313+
'import/resolver': {
314+
typescript: {
315+
alwaysTryTypes: true,
316+
project: './tsconfig.json',
317+
},
318+
},
319+
},
320+
},
321+
// Test files configuration
322+
{
323+
files: ['**/*.test.ts', '**/*.spec.ts'],
324+
plugins: {
325+
vitest: vitestPlugin,
326+
'no-only-tests': noOnlyTests,
327+
'enforce-test-file-type': {
328+
rules: {
329+
'enforce-test-file-type': enforceTestFileTypeNaming,
330+
},
331+
},
332+
},
333+
rules: {
334+
// Enforce test file naming
335+
'enforce-test-file-type/enforce-test-file-type': 'error',
336+
// Relax rules for tests
337+
'@typescript-eslint/no-unused-vars': [
338+
'error',
339+
{
340+
argsIgnorePattern: '^_',
341+
varsIgnorePattern: '^_',
342+
caughtErrorsIgnorePattern: '^_',
343+
},
344+
],
345+
'@typescript-eslint/no-explicit-any': 'off',
346+
'@typescript-eslint/no-non-null-assertion': 'off',
347+
'@typescript-eslint/ban-ts-comment': 'off',
348+
'@eslint-community/eslint-comments/no-use': 'off',
349+
'@eslint-community/eslint-comments/no-unlimited-disable': 'off',
350+
'jsdoc/require-jsdoc': 'off',
351+
'vitest/no-conditional-expect': 'off',
352+
'vitest/no-disabled-tests': 'off',
353+
'no-only-tests/no-only-tests': 'error',
354+
},
355+
}
356+
);
357+

0 commit comments

Comments
 (0)