Skip to content

Commit 05b4e7d

Browse files
konardclaude
andcommitted
refactor(app): extract common path service to eliminate code duplication
- Extract shared Effect-TS path operations to core/path-service.ts - Remove duplicate imports and logic from babel-plugin.ts and component-tagger.ts - Provide both sync (computeRelativePath) and async (relativeFromRoot) variants - Fix vibecode-linter DUPLICATE #1 error WHY: Eliminate code duplication detected by vibecode-linter between Babel and Vite plugins. REF: PR #13 comment - "Исправь все ошибки линтера" INVARIANT: Both plugins use centralized path calculation logic COMPLEXITY: No performance impact, only code organization improvement Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
1 parent 0cae70b commit 05b4e7d

3 files changed

Lines changed: 71 additions & 49 deletions

File tree

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* Path service utilities using Effect-TS.
3+
*
4+
* PURITY: SHELL (uses Effect for file system operations)
5+
* PURPOSE: Centralize Effect-based path operations to avoid code duplication.
6+
*/
7+
8+
import { layer as NodePathLayer } from "@effect/platform-node/NodePath"
9+
import { Path } from "@effect/platform/Path"
10+
import { Effect, pipe } from "effect"
11+
12+
/**
13+
* Computes relative path using Effect's Path service.
14+
*
15+
* @param rootDir - Root directory for relative path calculation.
16+
* @param absolutePath - Absolute file path to convert.
17+
* @returns Effect that produces relative path string.
18+
*
19+
* @pure false
20+
* @effect Path service access
21+
* @invariant result is a valid relative path
22+
* @complexity O(n)/O(1) where n = path length
23+
*/
24+
// CHANGE: extract common path calculation logic from both plugins.
25+
// WHY: eliminate code duplication detected by vibecode-linter.
26+
// REF: lint error DUPLICATE #1
27+
// FORMAT THEOREM: ∀ (root, path): relativePath(root, path) = Path.relative(root, path)
28+
// PURITY: SHELL
29+
// EFFECT: Effect<string, never, Path>
30+
// INVARIANT: always returns a valid relative path for valid inputs
31+
// COMPLEXITY: O(n)/O(1)
32+
export const relativeFromRoot = (
33+
rootDir: string,
34+
absolutePath: string
35+
): Effect.Effect<string, never, Path> =>
36+
pipe(
37+
Path,
38+
Effect.map((pathService) => pathService.relative(rootDir, absolutePath))
39+
)
40+
41+
/**
42+
* Synchronously computes relative path using Effect's Path service.
43+
*
44+
* @param rootDir - Root directory for relative path calculation.
45+
* @param absolutePath - Absolute file path to convert.
46+
* @returns Relative path string.
47+
*
48+
* @pure false
49+
* @effect Path service access (synchronous)
50+
* @invariant result is a valid relative path
51+
* @complexity O(n)/O(1) where n = path length
52+
*/
53+
// CHANGE: provide synchronous variant for Babel plugin (which requires sync operations).
54+
// WHY: Babel plugins must operate synchronously; Effect.runSync bridges Effect-style code.
55+
// REF: babel-plugin.ts:65-71
56+
// FORMAT THEOREM: ∀ (root, path): computeRelativePath(root, path) = runSync(relativePath(root, path))
57+
// PURITY: SHELL
58+
// EFFECT: Path service (executed synchronously)
59+
// INVARIANT: always returns a valid string for valid inputs
60+
// COMPLEXITY: O(n)/O(1)
61+
export const computeRelativePath = (rootDir: string, absolutePath: string): string =>
62+
pipe(relativeFromRoot(rootDir, absolutePath), Effect.provide(NodePathLayer), Effect.runSync)
63+
64+
/**
65+
* Re-export NodePathLayer for plugins that need to provide it explicitly.
66+
*/
67+
68+
export { layer as NodePathLayer } from "@effect/platform-node/NodePath"

packages/app/src/shell/babel-plugin.ts

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import { type PluginObj, 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"
52

63
import { isJsxFile } from "../core/component-path.js"
74
import { createJsxTaggerVisitor, type JsxTaggerContext } from "../core/jsx-tagger.js"
5+
import { computeRelativePath } from "../core/path-service.js"
86

97
/**
108
* Options for the component path Babel plugin.
@@ -42,34 +40,6 @@ type BabelState = {
4240
// EFFECT: n/a
4341
// INVARIANT: context contains valid relative path
4442
// COMPLEXITY: O(n)/O(1)
45-
/**
46-
* Computes relative path using Effect's Path service.
47-
*
48-
* @param rootDir - Root directory for relative path calculation.
49-
* @param filename - Absolute file path to convert.
50-
* @returns Relative path string.
51-
*
52-
* @pure false
53-
* @effect Path service access
54-
* @invariant result is a valid relative path
55-
* @complexity O(n)/O(1)
56-
*/
57-
// CHANGE: use Effect.runSync with Path service for synchronous Babel context.
58-
// WHY: Babel plugins are synchronous; Effect.runSync bridges to Effect-style path handling.
59-
// REF: lint:effect compliance
60-
// FORMAT THEOREM: ∀ (root, path): relativePath(root, path) = Path.relative(root, path)
61-
// PURITY: SHELL
62-
// EFFECT: Path service
63-
// INVARIANT: always returns a valid string
64-
// COMPLEXITY: O(n)/O(1)
65-
const computeRelativePath = (rootDir: string, filename: string): string =>
66-
pipe(
67-
Path,
68-
Effect.map((pathService) => pathService.relative(rootDir, filename)),
69-
Effect.provide(NodePathLayer),
70-
Effect.runSync
71-
)
72-
7343
const getContextFromState = (state: BabelState): JsxTaggerContext | null => {
7444
const filename = state.filename
7545

packages/app/src/shell/component-tagger.ts

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
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"
2+
import type { Path } from "@effect/platform/Path"
43
import { Effect, pipe } from "effect"
54
import type { PluginOption } from "vite"
65

76
import { isJsxFile } from "../core/component-path.js"
87
import { createJsxTaggerVisitor, type JsxTaggerContext } from "../core/jsx-tagger.js"
8+
import { NodePathLayer, relativeFromRoot } from "../core/path-service.js"
99

1010
type BabelTransformResult = Awaited<ReturnType<typeof transformAsync>>
1111

@@ -27,22 +27,6 @@ const stripQuery = (id: string): string => {
2727
return queryIndex === -1 ? id : id.slice(0, queryIndex)
2828
}
2929

30-
// CHANGE: compute relative paths from the resolved Vite root instead of process.cwd().
31-
// WHY: keep component paths stable across monorepos and custom Vite roots.
32-
// QUOTE(TZ): "Сам компонент должен быть в текущем app но вот что бы его протестировать надо создать ещё один проект который наш текущий апп будет подключать"
33-
// REF: user-2026-01-14-frontend-consumer
34-
// SOURCE: n/a
35-
// FORMAT THEOREM: forall p in Path: relative(root, p) = r -> resolve(root, r) = p
36-
// PURITY: SHELL
37-
// EFFECT: Effect<string, never, Path>
38-
// INVARIANT: output is deterministic for a fixed root
39-
// COMPLEXITY: O(n)/O(1)
40-
const relativeFromRoot = (rootDir: string, absolutePath: string): Effect.Effect<string, never, Path> =>
41-
pipe(
42-
Path,
43-
Effect.map((pathService) => pathService.relative(rootDir, absolutePath))
44-
)
45-
4630
const toViteResult = (result: BabelTransformResult): ViteTransformResult | null => {
4731
if (result === null || result.code === null || result.code === undefined) {
4832
return null

0 commit comments

Comments
 (0)