@prover-coder-ai/component-tagger 1.0.5 → 1.0.7

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,31 +1,20 @@
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)
7
+ cause;
8
+ _tag = "ComponentTaggerError";
9
+ constructor(message, cause) {
10
+ super(message);
11
+ this.cause = cause;
12
+ }
27
13
  }
28
-
14
+ const stripQuery = (id) => {
15
+ const queryIndex = id.indexOf("?");
16
+ return queryIndex === -1 ? id : id.slice(0, queryIndex);
17
+ };
29
18
  // CHANGE: compute relative paths from the resolved Vite root instead of process.cwd().
30
19
  // WHY: keep component paths stable across monorepos and custom Vite roots.
31
20
  // QUOTE(TZ): "Сам компонент должен быть в текущем app но вот что бы его протестировать надо создать ещё один проект который наш текущий апп будет подключать"
@@ -36,30 +25,18 @@ const stripQuery = (id: string): string => {
36
25
  // EFFECT: Effect<string, never, Path>
37
26
  // INVARIANT: output is deterministic for a fixed root
38
27
  // COMPLEXITY: O(n)/O(1)
39
- const relativeFromRoot = (rootDir: string, absolutePath: string): Effect.Effect<string, never, Path> =>
40
- pipe(
41
- Path,
42
- Effect.map((pathService) => pathService.relative(rootDir, absolutePath))
43
- )
44
-
45
- const attrExists = (node: t.JSXOpeningElement, attrName: string): boolean =>
46
- node.attributes.some(
47
- (attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, { name: attrName })
48
- )
49
-
50
- const toViteResult = (result: BabelTransformResult): ViteTransformResult | null => {
51
- if (result === null || result.code === null || result.code === undefined) {
52
- return null
53
- }
54
-
55
- const { code } = result
56
-
57
- return {
58
- code,
59
- map: result.map ?? null
60
- }
61
- }
62
-
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
+ };
63
40
  // CHANGE: inject a single path attribute into JSX opening elements.
64
41
  // WHY: remove redundant metadata while preserving the full source location payload.
65
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"
@@ -70,30 +47,23 @@ const toViteResult = (result: BabelTransformResult): ViteTransformResult | null
70
47
  // EFFECT: Effect<ViteTransformResult | null, ComponentTaggerError, Path>
71
48
  // INVARIANT: each JSX opening element has at most one path attribute
72
49
  // COMPLEXITY: O(n)/O(1), n = number of JSX elements
73
- const makeBabelTagger = (relativeFilename: string): PluginObj => ({
74
- name: "component-path-babel-tagger",
75
- visitor: {
76
- JSXOpeningElement(openPath: { readonly node: t.JSXOpeningElement }) {
77
- const { node } = openPath
78
-
79
- if (node.loc === null || node.loc === undefined) {
80
- return
81
- }
82
-
83
- if (attrExists(node, componentPathAttributeName)) {
84
- return
85
- }
86
-
87
- const { column, line } = node.loc.start
88
- const value = formatComponentPathValue(relativeFilename, line, column)
89
-
90
- node.attributes.push(
91
- t.jsxAttribute(t.jsxIdentifier(componentPathAttributeName), t.stringLiteral(value))
92
- )
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
+ }
93
65
  }
94
- }
95
- })
96
-
66
+ });
97
67
  /**
98
68
  * Builds a Vite transform result with a single component-path attribute per JSX element.
99
69
  *
@@ -116,39 +86,26 @@ const makeBabelTagger = (relativeFilename: string): PluginObj => ({
116
86
  // EFFECT: Effect<ViteTransformResult | null, ComponentTaggerError, never>
117
87
  // INVARIANT: errors are surfaced as ComponentTaggerError only
118
88
  // COMPLEXITY: O(n)/O(1)
119
- const runTransform = (
120
- code: string,
121
- id: string,
122
- rootDir: string
123
- ): Effect.Effect<ViteTransformResult | null, ComponentTaggerError, Path> => {
124
- const cleanId = stripQuery(id)
125
-
126
- return pipe(
127
- relativeFromRoot(rootDir, cleanId),
128
- Effect.flatMap((relative) =>
129
- Effect.tryPromise({
130
- try: () =>
131
- 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, {
132
93
  filename: cleanId,
133
94
  babelrc: false,
134
95
  configFile: false,
135
96
  parserOpts: {
136
- sourceType: "module",
137
- plugins: ["typescript", "jsx", "decorators-legacy"]
97
+ sourceType: "module",
98
+ plugins: ["typescript", "jsx", "decorators-legacy"]
138
99
  },
139
100
  plugins: [makeBabelTagger(relative)],
140
101
  sourceMaps: true
141
- }),
102
+ }),
142
103
  catch: (cause) => {
143
- const error = cause instanceof Error ? cause : new Error(String(cause))
144
- 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);
145
106
  }
146
- })
147
- ),
148
- Effect.map((result) => toViteResult(result))
149
- )
150
- }
151
-
107
+ })), Effect.map((result) => toViteResult(result)));
108
+ };
152
109
  /**
153
110
  * Creates a Vite plugin that injects a single component-path data attribute.
154
111
  *
@@ -170,22 +127,20 @@ const runTransform = (
170
127
  // EFFECT: Effect<ViteTransformResult | null, ComponentTaggerError, never>
171
128
  // INVARIANT: no duplicate path attributes
172
129
  // COMPLEXITY: O(n)/O(1)
173
- export const componentTagger = (): PluginOption => {
174
- let resolvedRoot = process.cwd()
175
-
176
- return {
177
- name: "component-path-tagger",
178
- enforce: "pre",
179
- apply: "serve",
180
- configResolved(config) {
181
- resolvedRoot = config.root
182
- },
183
- transform(code, id) {
184
- if (!isJsxFile(id)) {
185
- return null
186
- }
187
-
188
- return Effect.runPromise(pipe(runTransform(code, id, resolvedRoot), Effect.provide(NodePathLayer)))
189
- }
190
- }
191
- }
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
+ };
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@prover-coder-ai/component-tagger",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
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
  },
@@ -63,15 +66,16 @@
63
66
  "vitest": "^4.0.17"
64
67
  },
65
68
  "scripts": {
66
- "build": "tsc -p tsconfig.json --outDir dist --declaration --declarationMap false",
67
- "dev": "tsc -p tsconfig.json --watch --outDir dist --declaration --declarationMap false",
69
+ "prebuild": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
70
+ "build": "tsc -p tsconfig.build.json",
71
+ "dev": "tsc -p tsconfig.build.json --watch",
68
72
  "lint": "npx @ton-ai-core/vibecode-linter src/",
69
73
  "lint:tests": "npx @ton-ai-core/vibecode-linter tests/",
70
74
  "lint:effect": "npx eslint --config eslint.effect-ts-check.config.mjs .",
71
75
  "check": "pnpm run typecheck",
72
76
  "prestart": "pnpm run build",
73
77
  "start": "node dist/index.js",
74
- "test": "pnpm run lint:tests && vitest run",
78
+ "test": "pnpm run build && pnpm run lint:tests && vitest run",
75
79
  "typecheck": "tsc --noEmit"
76
80
  }
77
81
  }
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,31 +0,0 @@
1
- # @prover-coder-ai/component-tagger
2
-
3
- ## 1.0.5
4
-
5
- ### Patch Changes
6
-
7
- - chore: automated version bump
8
-
9
- ## 1.0.4
10
-
11
- ### Patch Changes
12
-
13
- - chore: automated version bump
14
-
15
- ## 1.0.3
16
-
17
- ### Patch Changes
18
-
19
- - chore: automated version bump
20
-
21
- ## 1.0.2
22
-
23
- ### Patch Changes
24
-
25
- - chore: automated version bump
26
-
27
- ## 1.0.1
28
-
29
- ### Patch Changes
30
-
31
- - 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
- })