Skip to content

Commit c2e6ef5

Browse files
authored
Merge pull request #20 from konard/issue-18-fe8f089190c9
refactor(core): extract normalizeModuleId for unified query stripping
2 parents 41389d2 + 64f9022 commit c2e6ef5

4 files changed

Lines changed: 62 additions & 11 deletions

File tree

packages/app/src/core/component-path.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,34 @@
11
const jsxFilePattern = /\.(tsx|jsx)(\?.*)?$/u
22

3+
/**
4+
* Normalizes a module ID by stripping query parameters.
5+
*
6+
* Vite and other bundlers may append query parameters to module IDs
7+
* (e.g., "src/App.tsx?import" or "src/App.tsx?v=123"). This function
8+
* returns the clean file path without query string.
9+
*
10+
* @param id - Module ID (may include query parameters).
11+
* @returns Clean path without query string.
12+
*
13+
* @pure true
14+
* @invariant ∀ id: normalizeModuleId(id) does not contain '?'
15+
* @complexity O(n) time / O(1) space where n = |id|
16+
*/
17+
// CHANGE: centralize query stripping as a pure function in core.
18+
// WHY: unify module ID normalization in one place as requested in issue #18.
19+
// QUOTE(ТЗ): "Вынести stripQuery() (или normalizeModuleId()) в core, использовать в Vite и (при желании) в isJsxFile."
20+
// REF: REQ-18 (issue #18)
21+
// SOURCE: n/a
22+
// FORMAT THEOREM: ∀ id: normalizeModuleId(id) = id.split('?')[0]
23+
// PURITY: CORE
24+
// EFFECT: n/a
25+
// INVARIANT: result contains no query string
26+
// COMPLEXITY: O(n)/O(1)
27+
export const normalizeModuleId = (id: string): string => {
28+
const queryIndex = id.indexOf("?")
29+
return queryIndex === -1 ? id : id.slice(0, queryIndex)
30+
}
31+
332
// CHANGE: rename attribute from "path" to "data-path" for HTML5 compliance.
433
// WHY: data-* attributes are standard HTML5 custom data attributes, improving compatibility.
534
// QUOTE(issue-14): "Rename attribute path → data-path (breaking change)"
@@ -24,7 +53,7 @@ export const componentPathAttributeName = "data-path"
2453
*/
2554
// CHANGE: centralize JSX file detection as a pure predicate.
2655
// 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"
56+
// QUOTE(TZ): "Сам компонент должен быть в текущем app но вот что бы его протестировать надо создать ещё один проект который наш текущий апп будет подключать"
2857
// REF: user-2026-01-14-frontend-consumer
2958
// SOURCE: n/a
3059
// FORMAT THEOREM: forall id in ModuleId: isJsxFile(id) -> matches(id, jsxFilePattern)
@@ -48,7 +77,7 @@ export const isJsxFile = (id: string): boolean => jsxFilePattern.test(id)
4877
*/
4978
// CHANGE: provide a pure formatter for component location payloads.
5079
// 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"
80+
// QUOTE(TZ): "Сам компонент должен быть в текущем app но вот что бы его протестировать надо создать ещё один проект который наш текущий апп будет подключать"
5281
// REF: user-2026-01-14-frontend-consumer
5382
// SOURCE: n/a
5483
// FORMAT THEOREM: forall p,l,c: formatComponentPathValue(p,l,c) = concat(p, ":", l, ":", c)

packages/app/src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
// EFFECT: n/a
99
// INVARIANT: exports remain stable for consumers
1010
// COMPLEXITY: O(1)/O(1)
11-
export { componentPathAttributeName, formatComponentPathValue, isJsxFile } from "./core/component-path.js"
11+
export {
12+
componentPathAttributeName,
13+
formatComponentPathValue,
14+
isJsxFile,
15+
normalizeModuleId
16+
} from "./core/component-path.js"
1217
export {
1318
attrExists,
1419
createJsxTaggerVisitor,

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

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Path } from "@effect/platform/Path"
33
import { Effect, pipe } from "effect"
44
import type { PluginOption } from "vite"
55

6-
import { componentPathAttributeName, isJsxFile } from "../core/component-path.js"
6+
import { componentPathAttributeName, isJsxFile, normalizeModuleId } from "../core/component-path.js"
77
import { createJsxTaggerVisitor, type JsxTaggerContext } from "../core/jsx-tagger.js"
88
import { NodePathLayer, relativeFromRoot } from "../core/path-service.js"
99

@@ -33,11 +33,6 @@ class ComponentTaggerError extends Error {
3333
}
3434
}
3535

36-
const stripQuery = (id: string): string => {
37-
const queryIndex = id.indexOf("?")
38-
return queryIndex === -1 ? id : id.slice(0, queryIndex)
39-
}
40-
4136
const toViteResult = (result: BabelTransformResult): ViteTransformResult | null => {
4237
if (result === null || result.code === null || result.code === undefined) {
4338
return null
@@ -105,7 +100,7 @@ const runTransform = (
105100
rootDir: string,
106101
attributeName: string
107102
): Effect.Effect<ViteTransformResult | null, ComponentTaggerError, Path> => {
108-
const cleanId = stripQuery(id)
103+
const cleanId = normalizeModuleId(id)
109104

110105
return pipe(
111106
relativeFromRoot(rootDir, cleanId),

packages/app/tests/core/component-path.test.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { describe, expect, it } from "@effect/vitest"
22
import { Effect } from "effect"
33

4-
import { componentPathAttributeName, formatComponentPathValue, isJsxFile } from "../../src/core/component-path.js"
4+
import {
5+
componentPathAttributeName,
6+
formatComponentPathValue,
7+
isJsxFile,
8+
normalizeModuleId
9+
} from "../../src/core/component-path.js"
510

611
describe("component-path", () => {
712
it.effect("exposes the data-path attribute name", () =>
@@ -21,4 +26,21 @@ describe("component-path", () => {
2126
expect(isJsxFile("src/App.jsx?import")).toBe(true)
2227
expect(isJsxFile("src/App.ts")).toBe(false)
2328
}))
29+
30+
it.effect("normalizes module id by stripping query string", () =>
31+
Effect.sync(() => {
32+
// With query parameter
33+
expect(normalizeModuleId("src/App.tsx?import")).toBe("src/App.tsx")
34+
expect(normalizeModuleId("src/App.jsx?v=123")).toBe("src/App.jsx")
35+
expect(normalizeModuleId("src/App.tsx?import&v=abc")).toBe("src/App.tsx")
36+
37+
// Without query parameter (idempotent)
38+
expect(normalizeModuleId("src/App.tsx")).toBe("src/App.tsx")
39+
expect(normalizeModuleId("src/App.jsx")).toBe("src/App.jsx")
40+
41+
// Edge cases
42+
expect(normalizeModuleId("")).toBe("")
43+
expect(normalizeModuleId("?")).toBe("")
44+
expect(normalizeModuleId("file?")).toBe("file")
45+
}))
2446
})

0 commit comments

Comments
 (0)