@prover-coder-ai/component-tagger 1.0.2

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.
package/.jscpd.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "threshold": 0,
3
+ "minTokens": 30,
4
+ "minLines": 5,
5
+ "ignore": [
6
+ "**/node_modules/**",
7
+ "**/build/**",
8
+ "**/dist/**",
9
+ "**/*.min.js",
10
+ "**/reports/**"
11
+ ],
12
+ "skipComments": true,
13
+ "ignorePattern": [
14
+ "private readonly \\w+: \\w+;\\s*private readonly \\w+: \\w+;\\s*private \\w+: \\w+ \\| null = null;\\s*private \\w+: \\w+ \\| null = null;"
15
+ ]
16
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # @prover-coder-ai/component-tagger
2
+
3
+ ## 1.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - chore: automated version bump
8
+
9
+ ## 1.0.1
10
+
11
+ ### Patch Changes
12
+
13
+ - chore: automated version bump
package/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # @prover-coder-ai/component-tagger
2
+
3
+ Vite plugin that adds a single `path` attribute to every JSX opening tag.
4
+
5
+ Example output:
6
+
7
+ ```html
8
+ <h1 path="src/App.tsx:22:4">Hello</h1>
9
+ ```
10
+
11
+ Format: `<relative-file-path>:<line>:<column>`
12
+
13
+ Recommended: enable only in `development` mode in Vite config.
package/biome.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
3
+ "vcs": {
4
+ "enabled": false,
5
+ "clientKind": "git",
6
+ "useIgnoreFile": false
7
+ },
8
+ "files": {
9
+ "ignoreUnknown": false
10
+ },
11
+ "formatter": {
12
+ "enabled": false,
13
+ "indentStyle": "tab"
14
+ },
15
+ "assist": {
16
+ "enabled": false
17
+ },
18
+ "linter": {
19
+ "enabled": false,
20
+ "rules": {
21
+ "recommended": false,
22
+ "suspicious": {
23
+ "noExplicitAny": "off"
24
+ }
25
+ }
26
+ },
27
+ "javascript": {
28
+ "formatter": {
29
+ "enabled": false,
30
+ "quoteStyle": "double",
31
+ "semicolons": "asNeeded"
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,305 @@
1
+ // eslint.config.mjs
2
+ // @ts-check
3
+ import eslint from '@eslint/js';
4
+ import { defineConfig } from 'eslint/config';
5
+ import tseslint from 'typescript-eslint';
6
+ import vitest from "@vitest/eslint-plugin";
7
+ import suggestMembers from "@prover-coder-ai/eslint-plugin-suggest-members";
8
+ import sonarjs from "eslint-plugin-sonarjs";
9
+ import unicorn from "eslint-plugin-unicorn";
10
+ import * as effectEslint from "@effect/eslint-plugin";
11
+ import { fixupPluginRules } from "@eslint/compat";
12
+ import codegen from "eslint-plugin-codegen";
13
+ import importPlugin from "eslint-plugin-import";
14
+ import simpleImportSort from "eslint-plugin-simple-import-sort";
15
+ import sortDestructureKeys from "eslint-plugin-sort-destructure-keys";
16
+ import globals from "globals";
17
+ import eslintCommentsConfigs from "@eslint-community/eslint-plugin-eslint-comments/configs";
18
+
19
+ const codegenPlugin = fixupPluginRules(
20
+ codegen as unknown as Parameters<typeof fixupPluginRules>[0],
21
+ );
22
+
23
+ const noFetchExample = [
24
+ "Пример:",
25
+ " import { FetchHttpClient, HttpClient } from \"@effect/platform\"",
26
+ " import { Effect } from \"effect\"",
27
+ " const program = Effect.gen(function* () {",
28
+ " const client = yield* HttpClient.HttpClient",
29
+ " return yield* client.get(`${api}/robots`)",
30
+ " }).pipe(",
31
+ " Effect.scoped,",
32
+ " Effect.provide(FetchHttpClient.layer)",
33
+ " )",
34
+ ].join("\n");
35
+
36
+ export default defineConfig(
37
+ eslint.configs.recommended,
38
+ tseslint.configs.strictTypeChecked,
39
+ effectEslint.configs.dprint,
40
+ suggestMembers.configs.recommended,
41
+ eslintCommentsConfigs.recommended,
42
+ {
43
+ name: "analyzers",
44
+ languageOptions: {
45
+ parser: tseslint.parser,
46
+ globals: { ...globals.node, ...globals.browser },
47
+ parserOptions: {
48
+ projectService: true,
49
+ tsconfigRootDir: import.meta.dirname,
50
+ },
51
+ },
52
+ plugins: {
53
+ sonarjs,
54
+ unicorn,
55
+ import: fixupPluginRules(importPlugin),
56
+ "sort-destructure-keys": sortDestructureKeys,
57
+ "simple-import-sort": simpleImportSort,
58
+ codegen: codegenPlugin,
59
+ },
60
+ files: ["**/*.ts", '**/*.{test,spec}.{ts,tsx}', '**/tests/**', '**/__tests__/**'],
61
+ settings: {
62
+ "import/parsers": {
63
+ "@typescript-eslint/parser": [".ts", ".tsx"],
64
+ },
65
+ "import/resolver": {
66
+ typescript: {
67
+ alwaysTryTypes: true,
68
+ },
69
+ },
70
+ },
71
+ rules: {
72
+ ...sonarjs.configs.recommended.rules,
73
+ ...unicorn.configs.recommended.rules,
74
+ "no-restricted-imports": ["error", {
75
+ paths: [
76
+ {
77
+ name: "ts-pattern",
78
+ message: "Use Effect.Match instead of ts-pattern.",
79
+ },
80
+ {
81
+ name: "zod",
82
+ message: "Use @effect/schema for schemas and validation.",
83
+ },
84
+ ],
85
+ }],
86
+ "codegen/codegen": "error",
87
+ "import/first": "error",
88
+ "import/newline-after-import": "error",
89
+ "import/no-duplicates": "error",
90
+ "import/no-unresolved": "off",
91
+ "import/order": "off",
92
+ "simple-import-sort/imports": "off",
93
+ "sort-destructure-keys/sort-destructure-keys": "error",
94
+ "no-fallthrough": "off",
95
+ "no-irregular-whitespace": "off",
96
+ "object-shorthand": "error",
97
+ "prefer-destructuring": "off",
98
+ "sort-imports": "off",
99
+ "no-unused-vars": "off",
100
+ "prefer-rest-params": "off",
101
+ "prefer-spread": "off",
102
+ "unicorn/prefer-top-level-await": "off",
103
+ "unicorn/prevent-abbreviations": "off",
104
+ "unicorn/no-null": "off",
105
+ complexity: ["error", 8],
106
+ "max-lines-per-function": [
107
+ "error",
108
+ { max: 50, skipBlankLines: true, skipComments: true },
109
+ ],
110
+ "max-params": ["error", 5],
111
+ "max-depth": ["error", 4],
112
+ "max-lines": [
113
+ "error",
114
+ { max: 300, skipBlankLines: true, skipComments: true },
115
+ ],
116
+
117
+ "@typescript-eslint/restrict-template-expressions": ["error", {
118
+ allowNumber: true,
119
+ allowBoolean: true,
120
+ allowNullish: false,
121
+ allowAny: false,
122
+ allowRegExp: false
123
+ }],
124
+ "@eslint-community/eslint-comments/no-use": "error",
125
+ "@eslint-community/eslint-comments/no-unlimited-disable": "error",
126
+ "@eslint-community/eslint-comments/disable-enable-pair": "error",
127
+ "@eslint-community/eslint-comments/no-unused-disable": "error",
128
+ "no-restricted-syntax": [
129
+ "error",
130
+ {
131
+ selector: "TSUnknownKeyword",
132
+ message: "Запрещено 'unknown'.",
133
+ },
134
+ // CHANGE: запрет прямого fetch в коде
135
+ // WHY: enforce Effect-TS httpClient as единственный источник сетевых эффектов
136
+ // QUOTE(ТЗ): "Вместо fetch должно быть всегда написано httpClient от библиотеки Effect-TS"
137
+ // REF: user-msg-1
138
+ // SOURCE: n/a
139
+ // FORMAT THEOREM: ∀call ∈ Calls: callee(call)=fetch → lint_error(call)
140
+ // PURITY: SHELL
141
+ // EFFECT: Effect<never, never, never>
142
+ // INVARIANT: direct fetch calls are forbidden
143
+ // COMPLEXITY: O(1)
144
+ {
145
+ selector: "CallExpression[callee.name='fetch']",
146
+ message: `Запрещён fetch — используй HttpClient (Effect-TS).\n${noFetchExample}`,
147
+ },
148
+ {
149
+ selector:
150
+ "CallExpression[callee.object.name='window'][callee.property.name='fetch']",
151
+ message: `Запрещён window.fetch — используй HttpClient (Effect-TS).\n${noFetchExample}`,
152
+ },
153
+ {
154
+ selector:
155
+ "CallExpression[callee.object.name='globalThis'][callee.property.name='fetch']",
156
+ message: `Запрещён globalThis.fetch — используй HttpClient (Effect-TS).\n${noFetchExample}`,
157
+ },
158
+ {
159
+ selector:
160
+ "CallExpression[callee.object.name='self'][callee.property.name='fetch']",
161
+ message: `Запрещён self.fetch — используй HttpClient (Effect-TS).\n${noFetchExample}`,
162
+ },
163
+ {
164
+ selector:
165
+ "CallExpression[callee.object.name='global'][callee.property.name='fetch']",
166
+ message: `Запрещён global.fetch — используй HttpClient (Effect-TS).\n${noFetchExample}`,
167
+ },
168
+ {
169
+ selector: "TryStatement",
170
+ message: "Используй Effect.try / catchAll вместо try/catch в core/app/domain.",
171
+ },
172
+ {
173
+ selector: "SwitchStatement",
174
+ message: [
175
+ "Switch statements are forbidden in functional programming paradigm.",
176
+ "How to fix: Use Effect.Match instead.",
177
+ "Example:",
178
+ " import { Match } from 'effect';",
179
+ " type Item = { type: 'this' } | { type: 'that' };",
180
+ " const result = Match.value(item).pipe(",
181
+ " Match.when({ type: 'this' }, (it) => processThis(it)),",
182
+ " Match.when({ type: 'that' }, (it) => processThat(it)),",
183
+ " Match.exhaustive,",
184
+ " );",
185
+ ].join("\n"),
186
+ },
187
+ {
188
+ selector: 'CallExpression[callee.name="require"]',
189
+ message: "Avoid using require(). Use ES6 imports instead.",
190
+ },
191
+ {
192
+ selector: "ThrowStatement > Literal:not([value=/^\\w+Error:/])",
193
+ message:
194
+ 'Do not throw string literals or non-Error objects. Throw new Error("...") instead.',
195
+ },
196
+ {
197
+ selector:
198
+ "FunctionDeclaration[async=true], FunctionExpression[async=true], ArrowFunctionExpression[async=true]",
199
+ message:
200
+ "Запрещён async/await — используй Effect.gen / Effect.tryPromise.",
201
+ },
202
+ {
203
+ selector: "NewExpression[callee.name='Promise']",
204
+ message:
205
+ "Запрещён new Promise — используй Effect.async / Effect.tryPromise.",
206
+ },
207
+ {
208
+ selector: "CallExpression[callee.object.name='Promise']",
209
+ message:
210
+ "Запрещены Promise.* — используй комбинаторы Effect (all, forEach, etc.).",
211
+ },
212
+ {
213
+ selector: "CallExpression[callee.property.name='push'] > SpreadElement.arguments",
214
+ message: "Do not use spread arguments in Array.push",
215
+ },
216
+ ],
217
+ "no-throw-literal": "error",
218
+ "@typescript-eslint/no-restricted-types": [
219
+ "error",
220
+ {
221
+ types: {
222
+ unknown: {
223
+ message:
224
+ "Не используем 'unknown'. Уточни тип или наведи порядок в источнике данных.",
225
+ },
226
+ Promise: {
227
+ message: "Запрещён Promise — используй Effect.Effect<A, E, R>.",
228
+ suggest: ["Effect.Effect"],
229
+ },
230
+ "Promise<*>": {
231
+ message:
232
+ "Запрещён Promise<T> — используй Effect.Effect<T, E, R>.",
233
+ suggest: ["Effect.Effect<T, E, R>"],
234
+ },
235
+ },
236
+ },
237
+ ],
238
+ "@typescript-eslint/use-unknown-in-catch-callback-variable": "off",
239
+ // "no-throw-literal": "off",
240
+ "@typescript-eslint/only-throw-error": [
241
+ "error",
242
+ { allowThrowingUnknown: false, allowThrowingAny: false },
243
+ ],
244
+ "@typescript-eslint/array-type": ["warn", {
245
+ default: "generic",
246
+ readonly: "generic"
247
+ }],
248
+ "@typescript-eslint/member-delimiter-style": 0,
249
+ "@typescript-eslint/no-non-null-assertion": "off",
250
+ "@typescript-eslint/ban-types": "off",
251
+ "@typescript-eslint/no-explicit-any": "off",
252
+ "@typescript-eslint/no-empty-interface": "off",
253
+ "@typescript-eslint/consistent-type-imports": "warn",
254
+ "@typescript-eslint/no-unused-vars": ["error", {
255
+ argsIgnorePattern: "^_",
256
+ varsIgnorePattern: "^_"
257
+ }],
258
+ "@typescript-eslint/ban-ts-comment": "off",
259
+ "@typescript-eslint/camelcase": "off",
260
+ "@typescript-eslint/explicit-function-return-type": "off",
261
+ "@typescript-eslint/explicit-module-boundary-types": "off",
262
+ "@typescript-eslint/interface-name-prefix": "off",
263
+ "@typescript-eslint/no-array-constructor": "off",
264
+ "@typescript-eslint/no-use-before-define": "off",
265
+ "@typescript-eslint/no-namespace": "off",
266
+ "@effect/dprint": ["error", {
267
+ config: {
268
+ indentWidth: 2,
269
+ lineWidth: 120,
270
+ semiColons: "asi",
271
+ quoteStyle: "alwaysDouble",
272
+ trailingCommas: "never",
273
+ operatorPosition: "maintain",
274
+ "arrowFunction.useParentheses": "force"
275
+ }
276
+ }]
277
+ }
278
+ },
279
+ {
280
+ files: ['**/*.{test,spec}.{ts,tsx}', 'tests/**', '**/__tests__/**'],
281
+ ...vitest.configs.all,
282
+ languageOptions: {
283
+ globals: {
284
+ ...vitest.environments.env.globals,
285
+ },
286
+ },
287
+ rules: {
288
+ // Allow eslint-disable/enable comments in test files for fine-grained control
289
+ '@eslint-community/eslint-comments/no-use': 'off',
290
+ // Disable line count limit for E2E tests that contain multiple test cases
291
+ 'max-lines-per-function': 'off',
292
+ // `it.effect` is not recognized by sonar rule; disable to avoid false positives
293
+ 'sonarjs/no-empty-test-file': 'off',
294
+ },
295
+ },
296
+
297
+ // 3) Для JS-файлов отключим типо-зависимые проверки
298
+ {
299
+ files: ['**/*.{js,cjs,mjs}'],
300
+ extends: [tseslint.configs.disableTypeChecked],
301
+ },
302
+
303
+ // 4) Глобальные игноры
304
+ { ignores: ['dist/**', 'build/**', 'coverage/**', '**/dist/**'] },
305
+ );
@@ -0,0 +1,220 @@
1
+ // CHANGE: add Effect-TS compliance lint profile
2
+ // WHY: detect current deviations from strict Effect-TS guidance
3
+ // QUOTE(TZ): n/a
4
+ // REF: AGENTS.md Effect-TS compliance checks
5
+ // SOURCE: n/a
6
+ // PURITY: SHELL
7
+ // EFFECT: eslint config
8
+ // INVARIANT: config only flags explicit policy deviations
9
+ // COMPLEXITY: O(1)/O(1)
10
+ import eslintComments from "@eslint-community/eslint-plugin-eslint-comments"
11
+ import globals from "globals"
12
+ import tseslint from "typescript-eslint"
13
+
14
+ const restrictedImports = [
15
+ {
16
+ name: "node:fs",
17
+ message: "Use @effect/platform FileSystem instead of node:fs."
18
+ },
19
+ {
20
+ name: "fs",
21
+ message: "Use @effect/platform FileSystem instead of fs."
22
+ },
23
+ {
24
+ name: "node:fs/promises",
25
+ message: "Use @effect/platform FileSystem instead of node:fs/promises."
26
+ },
27
+ {
28
+ name: "node:path/posix",
29
+ message: "Use @effect/platform Path instead of node:path/posix."
30
+ },
31
+ {
32
+ name: "node:path",
33
+ message: "Use @effect/platform Path instead of node:path."
34
+ },
35
+ {
36
+ name: "path",
37
+ message: "Use @effect/platform Path instead of path."
38
+ },
39
+ {
40
+ name: "node:child_process",
41
+ message: "Use @effect/platform Command instead of node:child_process."
42
+ },
43
+ {
44
+ name: "child_process",
45
+ message: "Use @effect/platform Command instead of child_process."
46
+ },
47
+ {
48
+ name: "node:process",
49
+ message: "Use @effect/platform Runtime instead of node:process."
50
+ },
51
+ {
52
+ name: "process",
53
+ message: "Use @effect/platform Runtime instead of process."
54
+ }
55
+ ]
56
+
57
+ const restrictedSyntaxBase = [
58
+ {
59
+ selector: "SwitchStatement",
60
+ message: "Switch is forbidden. Use Match.exhaustive."
61
+ },
62
+ {
63
+ selector: "TryStatement",
64
+ message: "Avoid try/catch in product code. Use Effect.try / Effect.catch*."
65
+ },
66
+ {
67
+ selector: "AwaitExpression",
68
+ message: "Avoid await. Use Effect.gen / Effect.flatMap."
69
+ },
70
+ {
71
+ selector: "FunctionDeclaration[async=true], FunctionExpression[async=true], ArrowFunctionExpression[async=true]",
72
+ message: "Avoid async/await. Use Effect.gen / Effect.tryPromise."
73
+ },
74
+ {
75
+ selector: "NewExpression[callee.name='Promise']",
76
+ message: "Avoid new Promise. Use Effect.async / Effect.tryPromise."
77
+ },
78
+ {
79
+ selector: "CallExpression[callee.object.name='Promise']",
80
+ message: "Avoid Promise.*. Use Effect combinators."
81
+ },
82
+ {
83
+ selector: "CallExpression[callee.name='require']",
84
+ message: "Avoid require(). Use ES module imports."
85
+ },
86
+ {
87
+ selector: "TSAsExpression",
88
+ message: "Casting is only allowed in src/core/axioms.ts."
89
+ },
90
+ {
91
+ selector: "TSTypeAssertion",
92
+ message: "Casting is only allowed in src/core/axioms.ts."
93
+ },
94
+ {
95
+ selector: "CallExpression[callee.name='makeFilesystemService']",
96
+ message: "Do not instantiate FilesystemService directly. Provide Layer and access via Tag."
97
+ },
98
+ {
99
+ selector: "CallExpression[callee.property.name='catchAll']",
100
+ message: "Avoid catchAll that discards typed errors; map or propagate explicitly."
101
+ }
102
+ ]
103
+
104
+ const restrictedSyntaxCore = [
105
+ ...restrictedSyntaxBase,
106
+ {
107
+ selector: "TSUnknownKeyword",
108
+ message: "unknown is allowed only at shell boundaries with decoding."
109
+ },
110
+ {
111
+ selector: "CallExpression[callee.property.name='runSyncExit']",
112
+ message: "Effect.runSyncExit is shell-only. Move to a runner."
113
+ },
114
+ {
115
+ selector: "CallExpression[callee.property.name='runSync']",
116
+ message: "Effect.runSync is shell-only. Move to a runner."
117
+ },
118
+ {
119
+ selector: "CallExpression[callee.property.name='runPromise']",
120
+ message: "Effect.runPromise is shell-only. Move to a runner."
121
+ }
122
+ ]
123
+
124
+ const restrictedSyntaxCoreNoAs = [
125
+ ...restrictedSyntaxCore.filter((rule) =>
126
+ rule.selector !== "TSAsExpression" && rule.selector !== "TSTypeAssertion"
127
+ )
128
+ ]
129
+
130
+ const restrictedSyntaxBaseNoServiceFactory = [
131
+ ...restrictedSyntaxBase.filter((rule) =>
132
+ rule.selector !== "CallExpression[callee.name='makeFilesystemService']"
133
+ )
134
+ ]
135
+
136
+ export default tseslint.config(
137
+ {
138
+ name: "effect-ts-compliance-check",
139
+ files: ["src/**/*.ts", "scripts/**/*.ts"],
140
+ languageOptions: {
141
+ parser: tseslint.parser,
142
+ globals: { ...globals.node }
143
+ },
144
+ plugins: {
145
+ "@typescript-eslint": tseslint.plugin,
146
+ "eslint-comments": eslintComments
147
+ },
148
+ rules: {
149
+ "no-console": "error",
150
+ "no-restricted-imports": ["error", {
151
+ paths: restrictedImports,
152
+ patterns: [
153
+ {
154
+ group: ["node:*"],
155
+ message: "Do not import from node:* directly. Use @effect/platform-node or @effect/platform services."
156
+ }
157
+ ]
158
+ }],
159
+ "no-restricted-syntax": ["error", ...restrictedSyntaxBase],
160
+ "@typescript-eslint/no-explicit-any": "error",
161
+ "@typescript-eslint/ban-ts-comment": ["error", {
162
+ "ts-ignore": true,
163
+ "ts-nocheck": true,
164
+ "ts-check": false,
165
+ "ts-expect-error": true
166
+ }],
167
+ "@typescript-eslint/no-restricted-types": ["error", {
168
+ types: {
169
+ Promise: {
170
+ message: "Avoid Promise in types. Use Effect.Effect<A, E, R>."
171
+ },
172
+ "Promise<*>": {
173
+ message: "Avoid Promise<T>. Use Effect.Effect<T, E, R>."
174
+ }
175
+ }
176
+ }],
177
+ "eslint-comments/no-use": "error",
178
+ "eslint-comments/no-unlimited-disable": "error",
179
+ "eslint-comments/disable-enable-pair": "error",
180
+ "eslint-comments/no-unused-disable": "error"
181
+ }
182
+ },
183
+ {
184
+ name: "effect-ts-compliance-core",
185
+ files: ["src/core/**/*.ts"],
186
+ rules: {
187
+ "no-restricted-syntax": ["error", ...restrictedSyntaxCore],
188
+ "no-restricted-imports": ["error", {
189
+ paths: restrictedImports,
190
+ patterns: [
191
+ {
192
+ group: [
193
+ "../shell/**",
194
+ "../../shell/**",
195
+ "../../../shell/**",
196
+ "./shell/**",
197
+ "src/shell/**",
198
+ "shell/**"
199
+ ],
200
+ message: "CORE must not import from SHELL."
201
+ }
202
+ ]
203
+ }]
204
+ }
205
+ },
206
+ {
207
+ name: "effect-ts-compliance-axioms",
208
+ files: ["src/core/axioms.ts"],
209
+ rules: {
210
+ "no-restricted-syntax": ["error", ...restrictedSyntaxCoreNoAs]
211
+ }
212
+ },
213
+ {
214
+ name: "effect-ts-compliance-filesystem-service",
215
+ files: ["src/shell/services/filesystem.ts"],
216
+ rules: {
217
+ "no-restricted-syntax": ["error", ...restrictedSyntaxBaseNoServiceFactory]
218
+ }
219
+ }
220
+ )
@@ -0,0 +1,33 @@
1
+ {
2
+ "priorityLevels": [
3
+ {
4
+ "level": 1,
5
+ "name": "Critical Compiler Errors",
6
+ "rules": [
7
+ "ts(2835)",
8
+ "ts(2307)",
9
+ "@prover-coder-ai/suggest-members/suggest-members",
10
+ "@prover-coder-ai/suggest-members/suggest-imports",
11
+ "@prover-coder-ai/suggest-members/suggest-module-paths",
12
+ "@prover-coder-ai/suggest-members/suggest-exports",
13
+ "@prover-coder-ai/suggest-members/suggest-missing-names",
14
+ "@typescript-eslint/no-explicit-any"
15
+ ]
16
+ },
17
+ {
18
+ "level": 2,
19
+ "name": "Critical Compiler Errors",
20
+ "rules": ["all"]
21
+ },
22
+ {
23
+ "level": 3,
24
+ "name": "Critical Compiler Errors (Code must follow Clean Code and best practices)",
25
+ "rules": ["max-lines-per-function", "max-lines"]
26
+ },
27
+ {
28
+ "level": 4,
29
+ "name": "Critical Compiler Errors (Code must follow Clean Code and best practices)",
30
+ "rules": ["complexity", "max-params", "max-depth"]
31
+ }
32
+ ]
33
+ }
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@prover-coder-ai/component-tagger",
3
+ "version": "1.0.2",
4
+ "description": "Component tagger Vite plugin for JSX metadata",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "directories": {
8
+ "doc": "doc"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/ProverCoderAI/effect-template.git"
13
+ },
14
+ "keywords": [
15
+ "effect",
16
+ "vite",
17
+ "plugin",
18
+ "tagger"
19
+ ],
20
+ "author": "",
21
+ "license": "ISC",
22
+ "type": "module",
23
+ "bugs": {
24
+ "url": "https://github.com/ProverCoderAI/effect-template/issues"
25
+ },
26
+ "homepage": "https://github.com/ProverCoderAI/effect-template#readme",
27
+ "dependencies": {
28
+ "@babel/core": "^7.28.6",
29
+ "@effect/platform": "^0.94.1",
30
+ "@effect/platform-node": "^0.104.0",
31
+ "effect": "^3.19.14"
32
+ },
33
+ "devDependencies": {
34
+ "@types/babel__core": "^7.20.5",
35
+ "@biomejs/biome": "^2.3.11",
36
+ "@effect/eslint-plugin": "^0.3.2",
37
+ "@effect/language-service": "latest",
38
+ "@effect/vitest": "^0.27.0",
39
+ "@eslint-community/eslint-plugin-eslint-comments": "^4.6.0",
40
+ "@eslint/compat": "2.0.1",
41
+ "@eslint/eslintrc": "3.3.3",
42
+ "@eslint/js": "9.39.2",
43
+ "@prover-coder-ai/eslint-plugin-suggest-members": "^0.0.13",
44
+ "@ton-ai-core/vibecode-linter": "^1.0.6",
45
+ "@types/node": "^24.10.8",
46
+ "@typescript-eslint/eslint-plugin": "^8.53.0",
47
+ "@typescript-eslint/parser": "^8.53.0",
48
+ "typescript-eslint": "^8.53.0",
49
+ "@vitest/coverage-v8": "^4.0.17",
50
+ "eslint": "^9.39.2",
51
+ "eslint-import-resolver-typescript": "^4.4.4",
52
+ "eslint-plugin-codegen": "0.34.1",
53
+ "eslint-plugin-import": "^2.32.0",
54
+ "eslint-plugin-simple-import-sort": "^12.1.1",
55
+ "eslint-plugin-sonarjs": "^3.0.5",
56
+ "eslint-plugin-sort-destructure-keys": "^2.0.0",
57
+ "eslint-plugin-unicorn": "^62.0.0",
58
+ "@vitest/eslint-plugin": "^1.6.6",
59
+ "globals": "^17.0.0",
60
+ "jscpd": "^4.0.7",
61
+ "typescript": "^5.9.3",
62
+ "vite": "^7.3.1",
63
+ "vitest": "^4.0.17"
64
+ },
65
+ "scripts": {
66
+ "build": "tsc -p tsconfig.json --outDir dist --declaration --declarationMap false",
67
+ "dev": "tsc -p tsconfig.json --watch --outDir dist --declaration --declarationMap false",
68
+ "lint": "npx @ton-ai-core/vibecode-linter src/",
69
+ "lint:tests": "npx @ton-ai-core/vibecode-linter tests/",
70
+ "lint:effect": "npx eslint --config eslint.effect-ts-check.config.mjs .",
71
+ "check": "pnpm run typecheck",
72
+ "prestart": "pnpm run build",
73
+ "start": "node dist/index.js",
74
+ "test": "pnpm run lint:tests && vitest run",
75
+ "typecheck": "tsc --noEmit"
76
+ }
77
+ }
@@ -0,0 +1,63 @@
1
+ const jsxFilePattern = /\.(tsx|jsx)(\?.*)?$/u
2
+
3
+ // CHANGE: define canonical attribute name for component path tagging.
4
+ // WHY: reduce metadata to a single attribute while keeping full source location.
5
+ // QUOTE(TZ): "\u0421\u0430\u043c \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0442\u0435\u043a\u0443\u0449\u0435\u043c app \u043d\u043e \u0432\u043e\u0442 \u0447\u0442\u043e \u0431\u044b \u0435\u0433\u043e \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043d\u0430\u0434\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0435\u0449\u0451 \u043e\u0434\u0438\u043d \u043f\u0440\u043e\u0435\u043a\u0442 \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0430\u0448 \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u0430\u043f\u043f \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0442\u044c"
6
+ // REF: user-2026-01-14-frontend-consumer
7
+ // SOURCE: n/a
8
+ // FORMAT THEOREM: forall a in AttributeName: a = "path"
9
+ // PURITY: CORE
10
+ // EFFECT: n/a
11
+ // INVARIANT: attribute name remains stable across transforms
12
+ // COMPLEXITY: O(1)/O(1)
13
+ export const componentPathAttributeName = "path"
14
+
15
+ /**
16
+ * Checks whether the Vite id represents a JSX or TSX module.
17
+ *
18
+ * @param id - Vite module id (may include query parameters).
19
+ * @returns true when the id ends with .jsx/.tsx (optionally with query).
20
+ *
21
+ * @pure true
22
+ * @invariant isJsxFile(id) = true -> id matches /\.(tsx|jsx)(\?.*)?$/u
23
+ * @complexity O(n) time / O(1) space where n = |id|
24
+ */
25
+ // CHANGE: centralize JSX file detection as a pure predicate.
26
+ // WHY: keep file filtering in the functional core for testability.
27
+ // QUOTE(TZ): "\u0421\u0430\u043c \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0442\u0435\u043a\u0443\u0449\u0435\u043c app \u043d\u043e \u0432\u043e\u0442 \u0447\u0442\u043e \u0431\u044b \u0435\u0433\u043e \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043d\u0430\u0434\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0435\u0449\u0451 \u043e\u0434\u0438\u043d \u043f\u0440\u043e\u0435\u043a\u0442 \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0430\u0448 \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u0430\u043f\u043f \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0442\u044c"
28
+ // REF: user-2026-01-14-frontend-consumer
29
+ // SOURCE: n/a
30
+ // FORMAT THEOREM: forall id in ModuleId: isJsxFile(id) -> matches(id, jsxFilePattern)
31
+ // PURITY: CORE
32
+ // EFFECT: n/a
33
+ // INVARIANT: predicate depends only on id content
34
+ // COMPLEXITY: O(n)/O(1)
35
+ export const isJsxFile = (id: string): boolean => jsxFilePattern.test(id)
36
+
37
+ /**
38
+ * Formats the component path payload containing file, line, and column.
39
+ *
40
+ * @param relativeFilename - Path relative to the project root.
41
+ * @param line - 1-based line number from the parser location.
42
+ * @param column - 0-based column number from the parser location.
43
+ * @returns Encoded location string: "<path>:<line>:<column>".
44
+ *
45
+ * @pure true
46
+ * @invariant line >= 1 and column >= 0
47
+ * @complexity O(1) time / O(1) space
48
+ */
49
+ // CHANGE: provide a pure formatter for component location payloads.
50
+ // WHY: reuse a single, deterministic encoding for UI metadata.
51
+ // QUOTE(TZ): "\u0421\u0430\u043c \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0442\u0435\u043a\u0443\u0449\u0435\u043c app \u043d\u043e \u0432\u043e\u0442 \u0447\u0442\u043e \u0431\u044b \u0435\u0433\u043e \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043d\u0430\u0434\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0435\u0449\u0451 \u043e\u0434\u0438\u043d \u043f\u0440\u043e\u0435\u043a\u0442 \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0430\u0448 \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u0430\u043f\u043f \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0442\u044c"
52
+ // REF: user-2026-01-14-frontend-consumer
53
+ // SOURCE: n/a
54
+ // FORMAT THEOREM: forall p,l,c: formatComponentPathValue(p,l,c) = concat(p, ":", l, ":", c)
55
+ // PURITY: CORE
56
+ // EFFECT: n/a
57
+ // INVARIANT: output encodes path + line + column without loss
58
+ // COMPLEXITY: O(1)/O(1)
59
+ export const formatComponentPathValue = (
60
+ relativeFilename: string,
61
+ line: number,
62
+ column: number
63
+ ): string => `${relativeFilename}:${line}:${column}`
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ // CHANGE: expose the component tagger as the library entrypoint.
2
+ // WHY: provide a single import surface for consumers.
3
+ // QUOTE(TZ): "\u0423\u0431\u0435\u0440\u0438 \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u0441\u0451 \u043b\u0438\u0448\u043d\u0438\u0435. \u0415\u0441\u043b\u0438 \u0447\u0442\u043e \u043c\u044b \u0434\u0435\u043b\u0430\u0435\u043c \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u0447\u043d\u044b\u0439 \u043c\u043e\u0434\u0443\u043b\u044c \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043f\u0440\u043e\u0441\u0442\u043e \u0431\u0443\u0434\u0435\u043c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0442\u044c \u0432 \u043f\u0440\u043e\u0435\u043a\u0442\u044b"
4
+ // REF: user-2026-01-14-library-cleanup
5
+ // SOURCE: n/a
6
+ // FORMAT THEOREM: forall consumer: import(index) -> available(componentTagger)
7
+ // PURITY: CORE
8
+ // EFFECT: n/a
9
+ // INVARIANT: exports remain stable for consumers
10
+ // COMPLEXITY: O(1)/O(1)
11
+ export { componentPathAttributeName, formatComponentPathValue, isJsxFile } from "./core/component-path.js"
12
+ export { componentTagger } from "./shell/component-tagger.js"
@@ -0,0 +1,170 @@
1
+ import { type PluginObj, transformAsync, types as t } from "@babel/core"
2
+ import { layer as NodePathLayer } from "@effect/platform-node/NodePath"
3
+ import { Path } from "@effect/platform/Path"
4
+ import { Effect, pipe } from "effect"
5
+ import type { PluginOption } from "vite"
6
+
7
+ import { componentPathAttributeName, formatComponentPathValue, isJsxFile } from "../core/component-path.js"
8
+
9
+ type BabelTransformResult = Awaited<ReturnType<typeof transformAsync>>
10
+
11
+ type ViteTransformResult = {
12
+ readonly code: string
13
+ readonly map: NonNullable<BabelTransformResult>["map"] | null
14
+ }
15
+
16
+ class ComponentTaggerError extends Error {
17
+ readonly _tag = "ComponentTaggerError"
18
+
19
+ constructor(message: string, override readonly cause: Error) {
20
+ super(message)
21
+ }
22
+ }
23
+
24
+ const stripQuery = (id: string): string => {
25
+ const queryIndex = id.indexOf("?")
26
+ return queryIndex === -1 ? id : id.slice(0, queryIndex)
27
+ }
28
+
29
+ const relativeFromCwd = (absolutePath: string): Effect.Effect<string, never, Path> =>
30
+ pipe(
31
+ Path,
32
+ Effect.map((pathService) => pathService.relative(process.cwd(), absolutePath))
33
+ )
34
+
35
+ const attrExists = (node: t.JSXOpeningElement, attrName: string): boolean =>
36
+ node.attributes.some(
37
+ (attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, { name: attrName })
38
+ )
39
+
40
+ const toViteResult = (result: BabelTransformResult): ViteTransformResult | null => {
41
+ if (result === null || result.code === null || result.code === undefined) {
42
+ return null
43
+ }
44
+
45
+ const { code } = result
46
+
47
+ return {
48
+ code,
49
+ map: result.map ?? null
50
+ }
51
+ }
52
+
53
+ // CHANGE: inject a single path attribute into JSX opening elements.
54
+ // WHY: remove redundant metadata while preserving the full source location payload.
55
+ // QUOTE(TZ): "\u0421\u0430\u043c \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0442\u0435\u043a\u0443\u0449\u0435\u043c app \u043d\u043e \u0432\u043e\u0442 \u0447\u0442\u043e \u0431\u044b \u0435\u0433\u043e \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043d\u0430\u0434\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0435\u0449\u0451 \u043e\u0434\u0438\u043d \u043f\u0440\u043e\u0435\u043a\u0442 \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0430\u0448 \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u0430\u043f\u043f \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0442\u044c"
56
+ // REF: user-2026-01-14-frontend-consumer
57
+ // SOURCE: n/a
58
+ // FORMAT THEOREM: forall f in JSXOpeningElement: rendered(f) -> annotated(f)
59
+ // PURITY: SHELL
60
+ // EFFECT: Effect<ViteTransformResult | null, ComponentTaggerError, Path>
61
+ // INVARIANT: each JSX opening element has at most one path attribute
62
+ // COMPLEXITY: O(n)/O(1), n = number of JSX elements
63
+ const makeBabelTagger = (relativeFilename: string): PluginObj => ({
64
+ name: "component-path-babel-tagger",
65
+ visitor: {
66
+ JSXOpeningElement(openPath: { readonly node: t.JSXOpeningElement }) {
67
+ const { node } = openPath
68
+
69
+ if (node.loc === null || node.loc === undefined) {
70
+ return
71
+ }
72
+
73
+ if (attrExists(node, componentPathAttributeName)) {
74
+ return
75
+ }
76
+
77
+ const { column, line } = node.loc.start
78
+ const value = formatComponentPathValue(relativeFilename, line, column)
79
+
80
+ node.attributes.push(
81
+ t.jsxAttribute(t.jsxIdentifier(componentPathAttributeName), t.stringLiteral(value))
82
+ )
83
+ }
84
+ }
85
+ })
86
+
87
+ /**
88
+ * Builds a Vite transform result with a single component-path attribute per JSX element.
89
+ *
90
+ * @param code - Source code to transform.
91
+ * @param id - Vite module id for the source code.
92
+ * @returns Vite-compatible transform result or null when no output is produced.
93
+ *
94
+ * @pure false
95
+ * @effect Babel transform
96
+ * @invariant each JSX opening element is tagged once per transform
97
+ * @complexity O(n) time / O(1) space where n = JSX element count
98
+ */
99
+ // CHANGE: wrap Babel transform in Effect for typed errors and controlled effects.
100
+ // WHY: satisfy the shell-only effect boundary while avoiding async/await.
101
+ // QUOTE(TZ): "\u0421\u0430\u043c \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0442\u0435\u043a\u0443\u0449\u0435\u043c app \u043d\u043e \u0432\u043e\u0442 \u0447\u0442\u043e \u0431\u044b \u0435\u0433\u043e \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043d\u0430\u0434\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0435\u0449\u0451 \u043e\u0434\u0438\u043d \u043f\u0440\u043e\u0435\u043a\u0442 \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0430\u0448 \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u0430\u043f\u043f \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0442\u044c"
102
+ // REF: user-2026-01-14-frontend-consumer
103
+ // SOURCE: n/a
104
+ // FORMAT THEOREM: forall c in Code: transform(c) = r -> r is tagged or null
105
+ // PURITY: SHELL
106
+ // EFFECT: Effect<ViteTransformResult | null, ComponentTaggerError, never>
107
+ // INVARIANT: errors are surfaced as ComponentTaggerError only
108
+ // COMPLEXITY: O(n)/O(1)
109
+ const runTransform = (
110
+ code: string,
111
+ id: string
112
+ ): Effect.Effect<ViteTransformResult | null, ComponentTaggerError, Path> => {
113
+ const cleanId = stripQuery(id)
114
+
115
+ return pipe(
116
+ relativeFromCwd(cleanId),
117
+ Effect.flatMap((relative) =>
118
+ Effect.tryPromise({
119
+ try: () =>
120
+ transformAsync(code, {
121
+ filename: cleanId,
122
+ parserOpts: {
123
+ sourceType: "module",
124
+ plugins: ["typescript", "jsx", "decorators-legacy"]
125
+ },
126
+ plugins: [makeBabelTagger(relative)],
127
+ sourceMaps: true
128
+ }),
129
+ catch: (cause) => {
130
+ const error = cause instanceof Error ? cause : new Error(String(cause))
131
+ return new ComponentTaggerError("Babel transform failed", error)
132
+ }
133
+ })
134
+ ),
135
+ Effect.map((result) => toViteResult(result))
136
+ )
137
+ }
138
+
139
+ /**
140
+ * Creates a Vite plugin that injects a single component-path data attribute.
141
+ *
142
+ * @returns Vite PluginOption for pre-transform tagging.
143
+ *
144
+ * @pure false
145
+ * @effect Babel transform through Effect
146
+ * @invariant only JSX/TSX modules are transformed
147
+ * @complexity O(n) time / O(1) space per JSX module
148
+ * @throws Never - errors are typed and surfaced by Effect
149
+ */
150
+ // CHANGE: expose a Vite plugin that tags JSX with only path.
151
+ // WHY: reduce attribute noise while keeping full path metadata.
152
+ // QUOTE(TZ): "\u0421\u0430\u043c \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0442\u0435\u043a\u0443\u0449\u0435\u043c app \u043d\u043e \u0432\u043e\u0442 \u0447\u0442\u043e \u0431\u044b \u0435\u0433\u043e \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043d\u0430\u0434\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0435\u0449\u0451 \u043e\u0434\u0438\u043d \u043f\u0440\u043e\u0435\u043a\u0442 \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0430\u0448 \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u0430\u043f\u043f \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0442\u044c"
153
+ // REF: user-2026-01-14-frontend-consumer
154
+ // SOURCE: n/a
155
+ // FORMAT THEOREM: forall id: isJsxFile(id) -> transform(id) adds component-path
156
+ // PURITY: SHELL
157
+ // EFFECT: Effect<ViteTransformResult | null, ComponentTaggerError, never>
158
+ // INVARIANT: no duplicate path attributes
159
+ // COMPLEXITY: O(n)/O(1)
160
+ export const componentTagger = (): PluginOption => ({
161
+ name: "component-path-tagger",
162
+ enforce: "pre",
163
+ transform(code, id) {
164
+ if (!isJsxFile(id)) {
165
+ return null
166
+ }
167
+
168
+ return Effect.runPromise(pipe(runTransform(code, id), Effect.provide(NodePathLayer)))
169
+ }
170
+ })
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from "@effect/vitest"
2
+ import { Effect } from "effect"
3
+
4
+ import { componentPathAttributeName, formatComponentPathValue, isJsxFile } from "../../src/core/component-path.js"
5
+
6
+ describe("component-path", () => {
7
+ it.effect("exposes the path attribute name", () =>
8
+ Effect.sync(() => {
9
+ expect(componentPathAttributeName).toBe("path")
10
+ }))
11
+
12
+ it.effect("formats the component path payload", () =>
13
+ Effect.sync(() => {
14
+ const result = formatComponentPathValue("src/App.tsx", 12, 3)
15
+ expect(result).toBe("src/App.tsx:12:3")
16
+ }))
17
+
18
+ it.effect("detects JSX/TSX module ids", () =>
19
+ Effect.sync(() => {
20
+ expect(isJsxFile("src/App.tsx")).toBe(true)
21
+ expect(isJsxFile("src/App.jsx?import")).toBe(true)
22
+ expect(isJsxFile("src/App.ts")).toBe(false)
23
+ }))
24
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": ".",
5
+ "outDir": "dist",
6
+ "types": ["vitest"],
7
+ "baseUrl": ".",
8
+ "paths": {
9
+ "@/*": ["src/*"]
10
+ }
11
+ },
12
+ "include": [
13
+ "src/**/*",
14
+ "tests/**/*",
15
+ "vitest.config.ts"
16
+ ],
17
+ "exclude": ["dist", "node_modules"]
18
+ }
@@ -0,0 +1,73 @@
1
+ // CHANGE: Migrate from Jest to Vitest with mathematical equivalence
2
+ // WHY: Faster execution, native ESM, Effect integration via @effect/vitest
3
+ // QUOTE(ТЗ): "Проект использует Effect + функциональную парадигму"
4
+ // REF: Migration from jest.config.mjs
5
+ // PURITY: SHELL (configuration only)
6
+ // INVARIANT: ∀ test: behavior_jest ≡ behavior_vitest
7
+ // EFFECT: Effect<TestReport, never, TestEnvironment>
8
+ // COMPLEXITY: O(n) test execution where n = |test_files|
9
+
10
+ import { defineConfig } from "vitest/config"
11
+
12
+ export default defineConfig({
13
+ test: {
14
+ // CHANGE: Native ESM support without experimental flags
15
+ // WHY: Vitest designed for ESM, no need for --experimental-vm-modules
16
+ // INVARIANT: Deterministic test execution without side effects
17
+ globals: false, // IMPORTANT: Use explicit imports for type safety
18
+ environment: "node",
19
+
20
+ // CHANGE: Match Jest's test file patterns
21
+ // INVARIANT: Same test discovery as Jest
22
+ include: ["tests/**/*.{test,spec}.ts"],
23
+ exclude: ["node_modules", "dist", "dist-test"],
24
+
25
+ // CHANGE: Coverage with 100% threshold for CORE (same as Jest)
26
+ // WHY: CORE must maintain mathematical guarantees via complete coverage
27
+ // INVARIANT: coverage_vitest ≥ coverage_jest ∧ ∀ f ∈ CORE: coverage(f) = 100%
28
+ coverage: {
29
+ provider: "v8", // Faster than babel (istanbul), native V8 coverage
30
+ reporter: ["text", "json", "html"],
31
+ include: ["src/**/*.ts"],
32
+ exclude: [
33
+ "src/**/*.test.ts",
34
+ "src/**/*.spec.ts",
35
+ "src/**/__tests__/**",
36
+ "scripts/**/*.ts"
37
+ ],
38
+ // CHANGE: Maintain exact same thresholds as Jest
39
+ // WHY: Enforce 100% coverage for CORE, 10% minimum for SHELL
40
+ // INVARIANT: ∀ f ∈ src/core/**/*.ts: all_metrics(f) = 100%
41
+ // NOTE: Vitest v8 provider collects coverage for all matched files by default
42
+ thresholds: {
43
+ "src/core/**/*.ts": {
44
+ branches: 100,
45
+ functions: 100,
46
+ lines: 100,
47
+ statements: 100
48
+ },
49
+ global: {
50
+ branches: 10,
51
+ functions: 10,
52
+ lines: 10,
53
+ statements: 10
54
+ }
55
+ }
56
+ },
57
+
58
+ // CHANGE: Faster test execution via thread pooling
59
+ // WHY: Vitest uses worker threads by default (faster than Jest's processes)
60
+ // COMPLEXITY: O(n/k) where n = tests, k = worker_count
61
+ // NOTE: Vitest runs tests in parallel by default, no additional config needed
62
+
63
+ // CHANGE: Clear mocks between tests (Jest equivalence)
64
+ // WHY: Prevent test contamination, ensure test independence
65
+ // INVARIANT: ∀ test_i, test_j: independent(test_i, test_j) ⇒ no_shared_state
66
+ clearMocks: true,
67
+ mockReset: true,
68
+ restoreMocks: true
69
+ // CHANGE: Disable globals to enforce explicit imports
70
+ // WHY: Type safety, explicit dependencies, functional purity
71
+ // NOTE: Tests must import { describe, it, expect } from "vitest"
72
+ }
73
+ })