@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 +16 -0
- package/CHANGELOG.md +13 -0
- package/README.md +13 -0
- package/biome.json +34 -0
- package/eslint.config.mts +305 -0
- package/eslint.effect-ts-check.config.mjs +220 -0
- package/linter.config.json +33 -0
- package/package.json +77 -0
- package/src/core/component-path.ts +63 -0
- package/src/index.ts +12 -0
- package/src/shell/component-tagger.ts +170 -0
- package/tests/core/component-path.test.ts +24 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +73 -0
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
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
|
+
}
|
package/vitest.config.ts
ADDED
|
@@ -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
|
+
})
|