@prover-coder-ai/component-tagger 1.0.4 → 1.0.6

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.
@@ -0,0 +1,25 @@
1
+ export declare const componentPathAttributeName = "path";
2
+ /**
3
+ * Checks whether the Vite id represents a JSX or TSX module.
4
+ *
5
+ * @param id - Vite module id (may include query parameters).
6
+ * @returns true when the id ends with .jsx/.tsx (optionally with query).
7
+ *
8
+ * @pure true
9
+ * @invariant isJsxFile(id) = true -> id matches /\.(tsx|jsx)(\?.*)?$/u
10
+ * @complexity O(n) time / O(1) space where n = |id|
11
+ */
12
+ export declare const isJsxFile: (id: string) => boolean;
13
+ /**
14
+ * Formats the component path payload containing file, line, and column.
15
+ *
16
+ * @param relativeFilename - Path relative to the project root.
17
+ * @param line - 1-based line number from the parser location.
18
+ * @param column - 0-based column number from the parser location.
19
+ * @returns Encoded location string: "<path>:<line>:<column>".
20
+ *
21
+ * @pure true
22
+ * @invariant line >= 1 and column >= 0
23
+ * @complexity O(1) time / O(1) space
24
+ */
25
+ export declare const formatComponentPathValue: (relativeFilename: string, line: number, column: number) => string;
@@ -1,5 +1,4 @@
1
- const jsxFilePattern = /\.(tsx|jsx)(\?.*)?$/u
2
-
1
+ const jsxFilePattern = /\.(tsx|jsx)(\?.*)?$/u;
3
2
  // CHANGE: define canonical attribute name for component path tagging.
4
3
  // WHY: reduce metadata to a single attribute while keeping full source location.
5
4
  // 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"
@@ -10,8 +9,7 @@ const jsxFilePattern = /\.(tsx|jsx)(\?.*)?$/u
10
9
  // EFFECT: n/a
11
10
  // INVARIANT: attribute name remains stable across transforms
12
11
  // COMPLEXITY: O(1)/O(1)
13
- export const componentPathAttributeName = "path"
14
-
12
+ export const componentPathAttributeName = "path";
15
13
  /**
16
14
  * Checks whether the Vite id represents a JSX or TSX module.
17
15
  *
@@ -32,8 +30,7 @@ export const componentPathAttributeName = "path"
32
30
  // EFFECT: n/a
33
31
  // INVARIANT: predicate depends only on id content
34
32
  // COMPLEXITY: O(n)/O(1)
35
- export const isJsxFile = (id: string): boolean => jsxFilePattern.test(id)
36
-
33
+ export const isJsxFile = (id) => jsxFilePattern.test(id);
37
34
  /**
38
35
  * Formats the component path payload containing file, line, and column.
39
36
  *
@@ -56,8 +53,4 @@ export const isJsxFile = (id: string): boolean => jsxFilePattern.test(id)
56
53
  // EFFECT: n/a
57
54
  // INVARIANT: output encodes path + line + column without loss
58
55
  // COMPLEXITY: O(1)/O(1)
59
- export const formatComponentPathValue = (
60
- relativeFilename: string,
61
- line: number,
62
- column: number
63
- ): string => `${relativeFilename}:${line}:${column}`
56
+ export const formatComponentPathValue = (relativeFilename, line, column) => `${relativeFilename}:${line}:${column}`;
@@ -0,0 +1,2 @@
1
+ export { componentPathAttributeName, formatComponentPathValue, isJsxFile } from "./core/component-path.js";
2
+ export { componentTagger } from "./shell/component-tagger.js";
@@ -8,5 +8,5 @@
8
8
  // EFFECT: n/a
9
9
  // INVARIANT: exports remain stable for consumers
10
10
  // COMPLEXITY: O(1)/O(1)
11
- export { componentPathAttributeName, formatComponentPathValue, isJsxFile } from "./core/component-path.js"
12
- export { componentTagger } from "./shell/component-tagger.js"
11
+ export { componentPathAttributeName, formatComponentPathValue, isJsxFile } from "./core/component-path.js";
12
+ export { componentTagger } from "./shell/component-tagger.js";
@@ -0,0 +1,13 @@
1
+ import type { PluginOption } from "vite";
2
+ /**
3
+ * Creates a Vite plugin that injects a single component-path data attribute.
4
+ *
5
+ * @returns Vite PluginOption for pre-transform tagging.
6
+ *
7
+ * @pure false
8
+ * @effect Babel transform through Effect
9
+ * @invariant only JSX/TSX modules are transformed
10
+ * @complexity O(n) time / O(1) space per JSX module
11
+ * @throws Never - errors are typed and surfaced by Effect
12
+ */
13
+ export declare const componentTagger: () => PluginOption;
@@ -1,55 +1,42 @@
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
-
1
+ import { 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 { componentPathAttributeName, formatComponentPathValue, isJsxFile } from "../core/component-path.js";
16
6
  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
- }
7
+ cause;
8
+ _tag = "ComponentTaggerError";
9
+ constructor(message, cause) {
10
+ super(message);
11
+ this.cause = cause;
12
+ }
51
13
  }
52
-
14
+ const stripQuery = (id) => {
15
+ const queryIndex = id.indexOf("?");
16
+ return queryIndex === -1 ? id : id.slice(0, queryIndex);
17
+ };
18
+ // CHANGE: compute relative paths from the resolved Vite root instead of process.cwd().
19
+ // WHY: keep component paths stable across monorepos and custom Vite roots.
20
+ // QUOTE(TZ): "Сам компонент должен быть в текущем app но вот что бы его протестировать надо создать ещё один проект который наш текущий апп будет подключать"
21
+ // REF: user-2026-01-14-frontend-consumer
22
+ // SOURCE: n/a
23
+ // FORMAT THEOREM: forall p in Path: relative(root, p) = r -> resolve(root, r) = p
24
+ // PURITY: SHELL
25
+ // EFFECT: Effect<string, never, Path>
26
+ // INVARIANT: output is deterministic for a fixed root
27
+ // COMPLEXITY: O(n)/O(1)
28
+ const relativeFromRoot = (rootDir, absolutePath) => pipe(Path, Effect.map((pathService) => pathService.relative(rootDir, absolutePath)));
29
+ const attrExists = (node, attrName) => node.attributes.some((attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, { name: attrName }));
30
+ const toViteResult = (result) => {
31
+ if (result === null || result.code === null || result.code === undefined) {
32
+ return null;
33
+ }
34
+ const { code } = result;
35
+ return {
36
+ code,
37
+ map: result.map ?? null
38
+ };
39
+ };
53
40
  // CHANGE: inject a single path attribute into JSX opening elements.
54
41
  // WHY: remove redundant metadata while preserving the full source location payload.
55
42
  // 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"
@@ -60,30 +47,23 @@ const toViteResult = (result: BabelTransformResult): ViteTransformResult | null
60
47
  // EFFECT: Effect<ViteTransformResult | null, ComponentTaggerError, Path>
61
48
  // INVARIANT: each JSX opening element has at most one path attribute
62
49
  // 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
- )
50
+ const makeBabelTagger = (relativeFilename) => ({
51
+ name: "component-path-babel-tagger",
52
+ visitor: {
53
+ JSXOpeningElement(openPath) {
54
+ const { node } = openPath;
55
+ if (node.loc === null || node.loc === undefined) {
56
+ return;
57
+ }
58
+ if (attrExists(node, componentPathAttributeName)) {
59
+ return;
60
+ }
61
+ const { column, line } = node.loc.start;
62
+ const value = formatComponentPathValue(relativeFilename, line, column);
63
+ node.attributes.push(t.jsxAttribute(t.jsxIdentifier(componentPathAttributeName), t.stringLiteral(value)));
64
+ }
83
65
  }
84
- }
85
- })
86
-
66
+ });
87
67
  /**
88
68
  * Builds a Vite transform result with a single component-path attribute per JSX element.
89
69
  *
@@ -106,36 +86,26 @@ const makeBabelTagger = (relativeFilename: string): PluginObj => ({
106
86
  // EFFECT: Effect<ViteTransformResult | null, ComponentTaggerError, never>
107
87
  // INVARIANT: errors are surfaced as ComponentTaggerError only
108
88
  // 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, {
89
+ const runTransform = (code, id, rootDir) => {
90
+ const cleanId = stripQuery(id);
91
+ return pipe(relativeFromRoot(rootDir, cleanId), Effect.flatMap((relative) => Effect.tryPromise({
92
+ try: () => transformAsync(code, {
121
93
  filename: cleanId,
94
+ babelrc: false,
95
+ configFile: false,
122
96
  parserOpts: {
123
- sourceType: "module",
124
- plugins: ["typescript", "jsx", "decorators-legacy"]
97
+ sourceType: "module",
98
+ plugins: ["typescript", "jsx", "decorators-legacy"]
125
99
  },
126
100
  plugins: [makeBabelTagger(relative)],
127
101
  sourceMaps: true
128
- }),
102
+ }),
129
103
  catch: (cause) => {
130
- const error = cause instanceof Error ? cause : new Error(String(cause))
131
- return new ComponentTaggerError("Babel transform failed", error)
104
+ const error = cause instanceof Error ? cause : new Error(String(cause));
105
+ return new ComponentTaggerError("Babel transform failed", error);
132
106
  }
133
- })
134
- ),
135
- Effect.map((result) => toViteResult(result))
136
- )
137
- }
138
-
107
+ })), Effect.map((result) => toViteResult(result)));
108
+ };
139
109
  /**
140
110
  * Creates a Vite plugin that injects a single component-path data attribute.
141
111
  *
@@ -157,14 +127,20 @@ const runTransform = (
157
127
  // EFFECT: Effect<ViteTransformResult | null, ComponentTaggerError, never>
158
128
  // INVARIANT: no duplicate path attributes
159
129
  // 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
- })
130
+ export const componentTagger = () => {
131
+ let resolvedRoot = process.cwd();
132
+ return {
133
+ name: "component-path-tagger",
134
+ enforce: "pre",
135
+ apply: "serve",
136
+ configResolved(config) {
137
+ resolvedRoot = config.root;
138
+ },
139
+ transform(code, id) {
140
+ if (!isJsxFile(id)) {
141
+ return null;
142
+ }
143
+ return Effect.runPromise(pipe(runTransform(code, id, resolvedRoot), Effect.provide(NodePathLayer)));
144
+ }
145
+ };
146
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from "@effect/vitest";
2
+ import { Effect } from "effect";
3
+ import { componentPathAttributeName, formatComponentPathValue, isJsxFile } from "../../src/core/component-path.js";
4
+ describe("component-path", () => {
5
+ it.effect("exposes the path attribute name", () => Effect.sync(() => {
6
+ expect(componentPathAttributeName).toBe("path");
7
+ }));
8
+ it.effect("formats the component path payload", () => Effect.sync(() => {
9
+ const result = formatComponentPathValue("src/App.tsx", 12, 3);
10
+ expect(result).toBe("src/App.tsx:12:3");
11
+ }));
12
+ it.effect("detects JSX/TSX module ids", () => Effect.sync(() => {
13
+ expect(isJsxFile("src/App.tsx")).toBe(true);
14
+ expect(isJsxFile("src/App.jsx?import")).toBe(true);
15
+ expect(isJsxFile("src/App.ts")).toBe(false);
16
+ }));
17
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("vite").UserConfig;
2
+ export default _default;
@@ -0,0 +1,67 @@
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
+ import { defineConfig } from "vitest/config";
10
+ export default defineConfig({
11
+ test: {
12
+ // CHANGE: Native ESM support without experimental flags
13
+ // WHY: Vitest designed for ESM, no need for --experimental-vm-modules
14
+ // INVARIANT: Deterministic test execution without side effects
15
+ globals: false, // IMPORTANT: Use explicit imports for type safety
16
+ environment: "node",
17
+ // CHANGE: Match Jest's test file patterns
18
+ // INVARIANT: Same test discovery as Jest
19
+ include: ["tests/**/*.{test,spec}.ts"],
20
+ exclude: ["node_modules", "dist", "dist-test"],
21
+ // CHANGE: Coverage with 100% threshold for CORE (same as Jest)
22
+ // WHY: CORE must maintain mathematical guarantees via complete coverage
23
+ // INVARIANT: coverage_vitest ≥ coverage_jest ∧ ∀ f ∈ CORE: coverage(f) = 100%
24
+ coverage: {
25
+ provider: "v8", // Faster than babel (istanbul), native V8 coverage
26
+ reporter: ["text", "json", "html"],
27
+ include: ["src/**/*.ts"],
28
+ exclude: [
29
+ "src/**/*.test.ts",
30
+ "src/**/*.spec.ts",
31
+ "src/**/__tests__/**",
32
+ "scripts/**/*.ts"
33
+ ],
34
+ // CHANGE: Maintain exact same thresholds as Jest
35
+ // WHY: Enforce 100% coverage for CORE, 10% minimum for SHELL
36
+ // INVARIANT: ∀ f ∈ src/core/**/*.ts: all_metrics(f) = 100%
37
+ // NOTE: Vitest v8 provider collects coverage for all matched files by default
38
+ thresholds: {
39
+ "src/core/**/*.ts": {
40
+ branches: 100,
41
+ functions: 100,
42
+ lines: 100,
43
+ statements: 100
44
+ },
45
+ global: {
46
+ branches: 10,
47
+ functions: 10,
48
+ lines: 10,
49
+ statements: 10
50
+ }
51
+ }
52
+ },
53
+ // CHANGE: Faster test execution via thread pooling
54
+ // WHY: Vitest uses worker threads by default (faster than Jest's processes)
55
+ // COMPLEXITY: O(n/k) where n = tests, k = worker_count
56
+ // NOTE: Vitest runs tests in parallel by default, no additional config needed
57
+ // CHANGE: Clear mocks between tests (Jest equivalence)
58
+ // WHY: Prevent test contamination, ensure test independence
59
+ // INVARIANT: ∀ test_i, test_j: independent(test_i, test_j) ⇒ no_shared_state
60
+ clearMocks: true,
61
+ mockReset: true,
62
+ restoreMocks: true
63
+ // CHANGE: Disable globals to enforce explicit imports
64
+ // WHY: Type safety, explicit dependencies, functional purity
65
+ // NOTE: Tests must import { describe, it, expect } from "vitest"
66
+ }
67
+ });
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@prover-coder-ai/component-tagger",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Component tagger Vite plugin for JSX metadata",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
7
10
  "directories": {
8
11
  "doc": "doc"
9
12
  },
package/.jscpd.json DELETED
@@ -1,16 +0,0 @@
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 DELETED
@@ -1,25 +0,0 @@
1
- # @prover-coder-ai/component-tagger
2
-
3
- ## 1.0.4
4
-
5
- ### Patch Changes
6
-
7
- - chore: automated version bump
8
-
9
- ## 1.0.3
10
-
11
- ### Patch Changes
12
-
13
- - chore: automated version bump
14
-
15
- ## 1.0.2
16
-
17
- ### Patch Changes
18
-
19
- - chore: automated version bump
20
-
21
- ## 1.0.1
22
-
23
- ### Patch Changes
24
-
25
- - chore: automated version bump
package/biome.json DELETED
@@ -1,34 +0,0 @@
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
- }
package/eslint.config.mts DELETED
@@ -1,305 +0,0 @@
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
- );
@@ -1,220 +0,0 @@
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
- )
@@ -1,33 +0,0 @@
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
- }
@@ -1,24 +0,0 @@
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 DELETED
@@ -1,18 +0,0 @@
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
- }
package/vitest.config.ts DELETED
@@ -1,73 +0,0 @@
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
- })