catlint 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/.editorconfig +13 -0
  2. package/.gitattributes +2 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.yml +41 -0
  4. package/.github/pull_request_template.md +31 -0
  5. package/.github/workflows/security.yaml +22 -0
  6. package/.husky/pre-commit +2 -0
  7. package/.prettierignore +3 -0
  8. package/.prettierrc +8 -0
  9. package/CODE_OF_CONDUCT.md +128 -0
  10. package/CONTRIBUTING.md +38 -0
  11. package/LICENSE +21 -0
  12. package/README.md +3 -0
  13. package/SECURITY.md +21 -0
  14. package/bun.lock +609 -0
  15. package/eslint.config.ts +45 -0
  16. package/legacy/group/model.ts +6 -0
  17. package/legacy/group/service.ts +13 -0
  18. package/legacy/index.ts +72 -0
  19. package/legacy/shared/yagni/index.ts +4 -0
  20. package/legacy/shared/yagni/noUnusedFeatures/index.ts +4 -0
  21. package/legacy/shared/yagni/noUnusedFeatures/main.ts +10 -0
  22. package/legacy/shared/yagni/noUnusedFeatures/noUnusedClasses.ts +26 -0
  23. package/legacy/shared/yagni/noUnusedFeatures/noUnusedFunctions.ts +26 -0
  24. package/legacy/shared/yagni/noUnusedFeatures/noUnusedVariables.ts +26 -0
  25. package/legacy/shared/yagni/utils.ts +5 -0
  26. package/package.json +51 -0
  27. package/src/cli/commands/init.ts +23 -0
  28. package/src/cli/commands/lint.ts +7 -0
  29. package/src/cli/index.ts +20 -0
  30. package/src/config/api/defineConfig.ts +10 -0
  31. package/src/config/api/loadConfig.ts +27 -0
  32. package/src/config/index.ts +3 -0
  33. package/src/config/types/config.ts +27 -0
  34. package/src/config/types/configAdapter.ts +10 -0
  35. package/src/core/api/lintProject.ts +20 -0
  36. package/src/core/api/linter.ts +22 -0
  37. package/src/core/models/lintResult/model.ts +8 -0
  38. package/src/ir/models/File.ts +7 -0
  39. package/src/ir/models/IRParser.ts +3 -0
  40. package/src/ir/models/Node.ts +14 -0
  41. package/src/rules/helpers/defaultAdapter.ts +20 -0
  42. package/src/rules/models/message/model.ts +11 -0
  43. package/src/rules/models/rule/model.ts +15 -0
  44. package/src/rules/models/rule/service.ts +14 -0
  45. package/src/shared/pattern/loadFiles.ts +17 -0
  46. package/tests/basic.test.ts +8 -0
  47. package/tsconfig.json +32 -0
  48. package/vitest.config.ts +12 -0
@@ -0,0 +1,45 @@
1
+ import js from '@eslint/js';
2
+ import globals from 'globals';
3
+ import tseslint from 'typescript-eslint';
4
+ import json from '@eslint/json';
5
+ import markdown from '@eslint/markdown';
6
+ import css from '@eslint/css';
7
+ import { defineConfig } from 'eslint/config';
8
+ import eslintConfigPrettier from 'eslint-config-prettier/flat';
9
+
10
+ export default defineConfig([
11
+ {
12
+ files: ['**/*.{js,mjs,cjs,ts,mts,cts}'],
13
+ plugins: { js },
14
+ extends: ['js/recommended'],
15
+ languageOptions: { globals: globals.browser },
16
+ },
17
+ tseslint.configs.recommended,
18
+ {
19
+ files: ['**/*.json'],
20
+ plugins: { json },
21
+ language: 'json/json',
22
+ extends: ['json/recommended'],
23
+ },
24
+ {
25
+ files: ['**/*.jsonc'],
26
+ plugins: { json },
27
+ language: 'json/jsonc',
28
+ extends: ['json/recommended'],
29
+ },
30
+ {
31
+ files: ['**/*.json5'],
32
+ plugins: { json },
33
+ language: 'json/json5',
34
+ extends: ['json/recommended'],
35
+ },
36
+ {
37
+ files: ['**/*.md'],
38
+ plugins: { markdown },
39
+ language: 'markdown/gfm',
40
+ extends: ['markdown/recommended'],
41
+ },
42
+ { files: ['**/*.css'], plugins: { css }, language: 'css/css', extends: ['css/recommended'] },
43
+ { ignores: ['**/dist/**', '**/legacy/**', '**/node_modules/**'] },
44
+ eslintConfigPrettier,
45
+ ]);
@@ -0,0 +1,6 @@
1
+ import type { Rule } from '@rules/$/models/rule/model';
2
+
3
+ export interface Group {
4
+ name: string;
5
+ rules: Rule[];
6
+ }
@@ -0,0 +1,13 @@
1
+ import type { Rule } from '@rules/$/models/rule/model';
2
+ import type { Group } from './model';
3
+
4
+ export const createGroup = (
5
+ name: string,
6
+ method: (push: (rule: Rule) => void, subgroup: (group: Group) => void) => void,
7
+ ): Group => {
8
+ const rules: Rule[] = [];
9
+ const push = (rule: Rule) => rules.push(rule);
10
+ const subgroup = (group: Group) => group.rules.forEach(push);
11
+ method(push, subgroup);
12
+ return { name, rules };
13
+ };
@@ -0,0 +1,72 @@
1
+ import { lintFile } from "@core/linter";
2
+ import type { LintResult } from "@core/models/lintResult/model";
3
+ import type { IRFile } from "@ir/$/models/File";
4
+ import { IRKind } from "@ir/$/models/Node";
5
+ import { yagniRules } from "@rules/yagni";
6
+ const file: IRFile = {
7
+ name: "test.ts",
8
+ extensions: ["ts"],
9
+ program: [
10
+ {
11
+ line: 1,
12
+ kind: IRKind.Codeline,
13
+ name: "test",
14
+ value: [
15
+ {
16
+ line: 2,
17
+ kind: IRKind.Function,
18
+ name: "test",
19
+ value: [
20
+ {
21
+ line: 3,
22
+ kind: IRKind.Variable,
23
+ name: "test",
24
+ value: [],
25
+ },
26
+ ],
27
+ },
28
+ ],
29
+ },
30
+ {
31
+ line: 4,
32
+ kind: IRKind.Class,
33
+ name: "test2",
34
+ value: [],
35
+ },
36
+
37
+ {
38
+ line: 5,
39
+ kind: IRKind.Variable,
40
+ name: "test9",
41
+ value: [],
42
+ },
43
+
44
+ {
45
+ line: 6,
46
+ kind: IRKind.Literal,
47
+ name: "test",
48
+ value: [],
49
+ },
50
+ ],
51
+ };
52
+
53
+ const rules = [yagniRules]
54
+
55
+ const lres = lintFile(file, rules);
56
+
57
+ console.log(JSON.stringify(lres, null, 2));
58
+
59
+ let indent = 0;
60
+
61
+ const print = (result: LintResult) => {
62
+ const char = result.isCorrect ? "✓" : "✗";
63
+ console.log(`${" ".repeat(indent)}${char} ${result.name}`);
64
+ indent += 2;
65
+ result.messages.forEach((message) => {
66
+ console.log(`${" ".repeat(indent)}[${message.level}] Line ${message.line}: ${message.message}`);
67
+ });
68
+ result.subrules.forEach(print);
69
+ indent -= 2;
70
+ }
71
+
72
+ lres.forEach(print);
@@ -0,0 +1,4 @@
1
+ import { createRule } from '@rules/models/rule/service';
2
+ import { noUnusedFeatures } from './noUnusedFeatures';
3
+
4
+ export const yagniRules = createRule('YAGNI', () => [], [noUnusedFeatures]);
@@ -0,0 +1,4 @@
1
+ export * from './noUnusedClasses';
2
+ export * from './noUnusedFunctions';
3
+ export * from './noUnusedVariables';
4
+ export * from './main';
@@ -0,0 +1,10 @@
1
+ import { noUnusedClasses } from './noUnusedClasses';
2
+ import { noUnusedFunctions } from './noUnusedFunctions';
3
+ import { noUnusedVariables } from './noUnusedVariables';
4
+ import { createRule } from '@rules/models/rule/service';
5
+
6
+ export const noUnusedFeatures = createRule('No unused features', () => [], [
7
+ noUnusedVariables,
8
+ noUnusedFunctions,
9
+ noUnusedClasses,
10
+ ]);
@@ -0,0 +1,26 @@
1
+ import { IRKind } from '@ir/models/Node';
2
+ import { Level, type Message } from '@rules/models/message/model';
3
+ import type { Rule } from '@rules/models/rule/model';
4
+ import { flattenNodes } from '../utils';
5
+ import { createRule } from '@rules/models/rule/service';
6
+
7
+ export const noUnusedClasses: Rule = createRule('No unused classes', (file) => {
8
+ const messages: Message[] = [];
9
+ const allNodes = flattenNodes(file.program);
10
+ const declaredClasses = allNodes.filter((n) => n.kind === IRKind.Class);
11
+ const referencedNames = new Set(
12
+ allNodes.filter((n) => n.kind !== IRKind.Class).map((n) => n.name),
13
+ );
14
+
15
+ for (const cls of declaredClasses) {
16
+ if (!referencedNames.has(cls.name)) {
17
+ messages.push({
18
+ message: `Class '${cls.name}' is declared but never used.`,
19
+ line: cls.line,
20
+ level: Level.Error,
21
+ });
22
+ }
23
+ }
24
+
25
+ return messages;
26
+ });
@@ -0,0 +1,26 @@
1
+ import { IRKind } from '@ir/models/Node';
2
+ import { Level, type Message } from '@rules/models/message/model';
3
+ import type { Rule } from '@rules/models/rule/model';
4
+ import { flattenNodes } from '../utils';
5
+ import { createRule } from '@rules/models/rule/service';
6
+
7
+ export const noUnusedFunctions: Rule = createRule('No unused functions', (file) => {
8
+ const messages: Message[] = [];
9
+ const allNodes = flattenNodes(file.program);
10
+ const declaredFunctions = allNodes.filter((n) => n.kind === IRKind.Function);
11
+ const referencedNames = new Set(
12
+ allNodes.filter((n) => n.kind !== IRKind.Function).map((n) => n.name),
13
+ );
14
+
15
+ for (const fn of declaredFunctions) {
16
+ if (!referencedNames.has(fn.name)) {
17
+ messages.push({
18
+ message: `Function '${fn.name}' is declared but never called.`,
19
+ line: fn.line,
20
+ level: Level.Error,
21
+ });
22
+ }
23
+ }
24
+
25
+ return messages;
26
+ });
@@ -0,0 +1,26 @@
1
+ import { IRKind } from '@ir/models/Node';
2
+ import { Level, type Message } from '@rules/models/message/model';
3
+ import type { Rule } from '@rules/models/rule/model';
4
+ import { flattenNodes } from '../utils';
5
+ import { createRule } from '@rules/models/rule/service';
6
+
7
+ export const noUnusedVariables: Rule = createRule('No unused variables', (file) => {
8
+ const messages: Message[] = [];
9
+ const allNodes = flattenNodes(file.program);
10
+ const declaredVariables = allNodes.filter((n) => n.kind === IRKind.Variable);
11
+ const referencedNames = new Set(
12
+ allNodes.filter((n) => n.kind !== IRKind.Variable).map((n) => n.name),
13
+ );
14
+
15
+ for (const variable of declaredVariables) {
16
+ if (!referencedNames.has(variable.name)) {
17
+ messages.push({
18
+ message: `Variable '${variable.name}' is declared but never used.`,
19
+ line: variable.line,
20
+ level: Level.Error,
21
+ });
22
+ }
23
+ }
24
+
25
+ return messages;
26
+ });
@@ -0,0 +1,5 @@
1
+ import type { IRNode } from '@ir/$/models/Node';
2
+
3
+ export function flattenNodes(nodes: IRNode[]): IRNode[] {
4
+ return nodes.flatMap((node) => [node, ...flattenNodes(node.value)]);
5
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "catlint",
3
+ "module": "src/index.ts",
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "lint": "eslint src --ext .ts",
8
+ "format": "prettier --write src",
9
+ "check": "bun run lint && bun run format",
10
+ "test": "vitest",
11
+ "prepare": "husky"
12
+ },
13
+ "engines": {
14
+ "node": "^5.3.0"
15
+ },
16
+ "devDependencies": {
17
+ "@eslint/css": "^1.0.0",
18
+ "@eslint/js": "^10.0.1",
19
+ "@eslint/json": "^1.2.0",
20
+ "@eslint/markdown": "^8.0.0",
21
+ "@types/bun": "latest",
22
+ "@types/node": "^25.5.0",
23
+ "@typescript-eslint/eslint-plugin": "^8.57.2",
24
+ "@typescript-eslint/parser": "^8.57.2",
25
+ "eslint": "^10.1.0",
26
+ "eslint-config-prettier": "^10.1.8",
27
+ "globals": "^17.4.0",
28
+ "husky": "^9.1.7",
29
+ "prettier": "3.8.1",
30
+ "typescript-eslint": "^8.57.2",
31
+ "vitest": "^4.1.1"
32
+ },
33
+ "peerDependencies": {
34
+ "typescript": "^5"
35
+ },
36
+ "lint-staged": {
37
+ "*.{ts,js}": [
38
+ "eslint --fix",
39
+ "prettier --write"
40
+ ]
41
+ },
42
+ "dependencies": {
43
+ "catlint": ".",
44
+ "chalk": "^5.6.2",
45
+ "commander": "^14.0.3",
46
+ "glob": "^13.0.6",
47
+ "inquirer": "^13.3.2",
48
+ "jiti": "^2.6.1",
49
+ "minimatch": "^10.2.5"
50
+ }
51
+ }
@@ -0,0 +1,23 @@
1
+ import inquirer from 'inquirer';
2
+ import fs from 'fs';
3
+ import { defaultConfig } from '@config/index';
4
+ import { inspect } from 'util';
5
+
6
+ export const init = async () => {
7
+ const { useTypescript } = await inquirer.prompt([
8
+ {
9
+ type: 'confirm',
10
+ name: 'useTypescript',
11
+ message: 'Do you want to use typescript?',
12
+ default: true,
13
+ },
14
+ ]);
15
+
16
+ fs.writeFileSync(
17
+ `catlint.config.${useTypescript ? 'ts' : 'js'}`,
18
+ `import { defineConfig } from 'catlint/config';
19
+
20
+ export default defineConfig(${inspect(defaultConfig)});
21
+ `,
22
+ );
23
+ };
@@ -0,0 +1,7 @@
1
+ import { defaultConfig, loadConfig } from '@config/index';
2
+ import { lintProject } from '@core/api/lintProject';
3
+
4
+ export const lint = async () => {
5
+ const config = (await loadConfig('.')) ?? defaultConfig;
6
+ lintProject(config, '.');
7
+ };
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { init } from './commands/init';
4
+ import { lint } from './commands/lint';
5
+
6
+ const program = new Command();
7
+
8
+ program.name('catlint').description('A linter for detecting code bad practices').version('0.0.1');
9
+
10
+ program
11
+ .command('init')
12
+ .description('Inits CatLint in your current project folder')
13
+ .action(async () => await init());
14
+
15
+ program
16
+ .command('lint')
17
+ .description('Lints the current project')
18
+ .action(async () => await lint());
19
+
20
+ program.parse();
@@ -0,0 +1,10 @@
1
+ import type { Config, NormalizedConfig } from '@config/types/config';
2
+ import { defaultAdapter } from '@rules/helpers/defaultAdapter';
3
+
4
+ export function defineConfig(config: Config): NormalizedConfig {
5
+ const normalizedConfig: NormalizedConfig = {
6
+ ...config,
7
+ rules: config.rules.map((rule) => defaultAdapter(rule)),
8
+ };
9
+ return normalizedConfig;
10
+ }
@@ -0,0 +1,27 @@
1
+ import type { NormalizedConfig } from '@config/types/config';
2
+ import { adaptConfig } from '@config/types/configAdapter';
3
+ import path from 'path';
4
+ import { pathToFileURL } from 'url';
5
+
6
+ export const loadConfigFromFile = async (filePath: string): Promise<NormalizedConfig | null> => {
7
+ try {
8
+ const config = await import(pathToFileURL(filePath).href);
9
+ return adaptConfig(config.default);
10
+ } catch {
11
+ console.log(`No config file found at ${filePath}`);
12
+ return null;
13
+ }
14
+ };
15
+
16
+ export const loadConfig = async (projectPath: string): Promise<NormalizedConfig> => {
17
+ const configPath = path.join(process.cwd(), projectPath) + '/catlint.config';
18
+
19
+ const config =
20
+ (await loadConfigFromFile(configPath + '.js')) ??
21
+ (await loadConfigFromFile(configPath + '.ts'));
22
+ if (!config) {
23
+ throw new Error('No config file found');
24
+ }
25
+
26
+ return config;
27
+ };
@@ -0,0 +1,3 @@
1
+ export * from './types/config';
2
+ export * from './api/defineConfig';
3
+ export * from './api/loadConfig';
@@ -0,0 +1,27 @@
1
+ import type { IRParser } from '@ir/models/IRParser';
2
+ import type { AdaptedRule, Rule } from '@rules/models/rule/model';
3
+
4
+ export interface Config {
5
+ lint: {
6
+ includes: string[];
7
+ excludes: string[];
8
+ };
9
+ parsers: {
10
+ pattern: string;
11
+ parser: IRParser;
12
+ }[];
13
+ rules: Rule[];
14
+ }
15
+
16
+ export interface NormalizedConfig extends Config {
17
+ rules: AdaptedRule[];
18
+ }
19
+
20
+ export const defaultConfig: NormalizedConfig = {
21
+ lint: {
22
+ includes: ['**/*.ts'],
23
+ excludes: ['node_modules/**'],
24
+ },
25
+ parsers: [],
26
+ rules: [],
27
+ };
@@ -0,0 +1,10 @@
1
+ import { defaultAdapter } from '@rules/helpers/defaultAdapter';
2
+ import type { Config, NormalizedConfig } from './config';
3
+
4
+ export const adaptConfig: (c: Config) => NormalizedConfig = (config: Config) => {
5
+ const normalizedConfig: NormalizedConfig = {
6
+ ...config,
7
+ rules: config.rules.map((rule) => defaultAdapter(rule)),
8
+ };
9
+ return normalizedConfig;
10
+ };
@@ -0,0 +1,20 @@
1
+ import type { NormalizedConfig } from '@config/types/config';
2
+ import { loadFiles } from 'src/shared/pattern/loadFiles';
3
+ import { lintFile } from './linter';
4
+ import type { IRFile } from '@ir/models/File';
5
+ import fs from 'fs';
6
+ import { Minimatch } from 'minimatch';
7
+
8
+ export const lintProject = async (config: NormalizedConfig, projectDir: string) => {
9
+ const files = await loadFiles(projectDir, config.lint.includes, config.lint.excludes);
10
+ return files
11
+ .map((filePath: string) => {
12
+ const fileContent = fs.readFileSync(filePath, 'utf-8');
13
+ const parser = config.parsers.find((parser) =>
14
+ new Minimatch(parser.pattern).match(filePath),
15
+ );
16
+ if (!parser) console.error(`No parser found for file ${filePath}`);
17
+ return parser?.parser(fileContent, filePath) ?? ({} as IRFile);
18
+ })
19
+ .map((file: IRFile) => lintFile(file, config.rules));
20
+ };
@@ -0,0 +1,22 @@
1
+ import type { LintResult } from '@core/models/lintResult/model';
2
+ import type { IRFile } from '@ir/models/File';
3
+ import type { AdaptedRule } from '@rules/models/rule/model';
4
+
5
+ export function travelFile(file: IRFile) {
6
+ const travelRule: (rule: AdaptedRule) => LintResult = (rule) => {
7
+ return {
8
+ name: rule.name,
9
+ messages: rule.fn(file),
10
+ subrules: rule.subrules.map(travelRule),
11
+ isCorrect:
12
+ rule.fn(file).length === 0 && rule.subrules.every((r) => travelRule(r).isCorrect),
13
+ };
14
+ };
15
+ return travelRule;
16
+ }
17
+
18
+ export function lintFile(file: IRFile, rules: AdaptedRule[]): LintResult[] {
19
+ const results: LintResult[] = rules.map((rule) => travelFile(file)(rule));
20
+
21
+ return results;
22
+ }
@@ -0,0 +1,8 @@
1
+ import type { Message } from '@rules/models/message/model';
2
+
3
+ export type LintResult = {
4
+ name: string;
5
+ messages: Message[];
6
+ subrules: LintResult[];
7
+ isCorrect: boolean;
8
+ };
@@ -0,0 +1,7 @@
1
+ import type { IRNode } from './Node';
2
+
3
+ export interface IRFile {
4
+ name: string;
5
+ extensions: string[];
6
+ program: IRNode[];
7
+ }
@@ -0,0 +1,3 @@
1
+ import type { IRFile } from './File';
2
+
3
+ export type IRParser = (filename: string, content: string) => IRFile;
@@ -0,0 +1,14 @@
1
+ export enum IRKind {
2
+ Codeline,
3
+ Literal,
4
+ Function,
5
+ Class,
6
+ Variable,
7
+ }
8
+
9
+ export interface IRNode {
10
+ line: number;
11
+ kind: IRKind;
12
+ name: string;
13
+ value: IRNode[];
14
+ }
@@ -0,0 +1,20 @@
1
+ import type { AdaptedRule, Rule } from '@rules/models/rule/model';
2
+
3
+ const EMPTY_FN = () => [];
4
+
5
+ /**
6
+ * @function defaultAdapter recursively adapts a Rule to an AdaptedRule by providing default values for undefined/null fields.
7
+ *
8
+ * @param rule the rule to adapt
9
+ * @returns a AdaptedRule without undefined/null fields
10
+ *
11
+ */
12
+ export const defaultAdapter = (rule: Rule): AdaptedRule => {
13
+ const { name, fn = EMPTY_FN, subrules = [] } = rule;
14
+
15
+ return {
16
+ name,
17
+ fn,
18
+ subrules: subrules.map(defaultAdapter),
19
+ };
20
+ };
@@ -0,0 +1,11 @@
1
+ export enum Level {
2
+ Error = 'ERROR',
3
+ Warning = 'WARNING',
4
+ Info = 'INFO',
5
+ }
6
+
7
+ export interface Message {
8
+ message: string;
9
+ line: number;
10
+ level: Level;
11
+ }
@@ -0,0 +1,15 @@
1
+ import type { IRFile } from '@ir/models/File';
2
+ import type { Message } from '@rules/models/message/model';
3
+
4
+ export type RuleFn = (file: IRFile) => Message[];
5
+
6
+ export interface Rule {
7
+ name: string;
8
+ fn?: RuleFn;
9
+ subrules?: Rule[];
10
+ }
11
+
12
+ export interface AdaptedRule extends Rule {
13
+ fn: RuleFn;
14
+ subrules: AdaptedRule[];
15
+ }
@@ -0,0 +1,14 @@
1
+ import type { Rule, RuleFn } from './model';
2
+
3
+ export const createRule = (
4
+ name: string,
5
+ fn: (subrule: (subrule: Rule) => void) => RuleFn,
6
+ ): Rule => {
7
+ const subrules: Rule[] = [];
8
+ const ruleFn: RuleFn = fn((subrule: Rule) => subrules.push(subrule));
9
+ return {
10
+ name,
11
+ fn: ruleFn,
12
+ subrules,
13
+ };
14
+ };
@@ -0,0 +1,17 @@
1
+ import { glob } from 'glob';
2
+
3
+ export const loadFiles = async (baseDir: string, includes: string[], excludes: string[] = []) => {
4
+ const included = (
5
+ await Promise.all(
6
+ includes.map((pattern) => glob(pattern, { cwd: baseDir, absolute: true })),
7
+ )
8
+ ).flat();
9
+
10
+ const excluded = (
11
+ await Promise.all(
12
+ excludes.map((pattern) => glob(pattern, { cwd: baseDir, absolute: true })),
13
+ )
14
+ ).flat();
15
+
16
+ return included.filter((file) => !excluded.includes(file));
17
+ };
@@ -0,0 +1,8 @@
1
+ import { expect, it, describe } from 'vitest';
2
+
3
+ // Basic and useless test
4
+ describe('basic', () => {
5
+ it('should work', () => {
6
+ expect(true).toBe(true);
7
+ });
8
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".",
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "ESNext",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "noEmit": true,
15
+
16
+ "strict": true,
17
+ "skipLibCheck": true,
18
+ "noFallthroughCasesInSwitch": true,
19
+ "noUncheckedIndexedAccess": true,
20
+ "noImplicitOverride": true,
21
+
22
+ "noUnusedLocals": true,
23
+ "noUnusedParameters": true,
24
+
25
+ "paths": {
26
+ "@core/*": ["./src/core/*"],
27
+ "@rules/*": ["./src/rules/*"],
28
+ "@ir/*": ["./src/ir/*"],
29
+ "@config/*": ["./src/config/*"]
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,12 @@
1
+ /// <reference types="vitest" />
2
+ import { defineConfig } from 'vitest/config';
3
+
4
+ export default defineConfig({
5
+ test: {
6
+ globals: true,
7
+ environment: 'node',
8
+ coverage: {
9
+ reporter: ['text', 'lcov'],
10
+ },
11
+ },
12
+ });