@7n/rules 1.37.0 → 1.39.0
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/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/rules/tauri/core_test_isolation/core_test_isolation.mdc +21 -7
- package/rules/tauri/core_test_isolation/docs/main.md +1 -1
- package/rules/tauri/core_test_isolation/main.mjs +12 -27
- package/rules/tauri/gitignore_target/docs/main.md +1 -2
- package/rules/tauri/gitignore_target/gitignore_target.mdc +25 -6
- package/rules/tauri/gitignore_target/main.mjs +28 -13
- package/scripts/utils/cargo-workspace.mjs +99 -0
- package/scripts/utils/docs/cargo-workspace.md +40 -0
- package/scripts/utils/docs/index.md +2 -0
- package/scripts/utils/docs/uv-workspace.md +34 -0
- package/scripts/utils/uv-workspace.mjs +56 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.39.0] - 2026-07-21
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- workspace_root: канон одного кореневого uv workspace на репозиторій (дзеркало rust/workspace_root), main.json auto-glob розширено на вкладені pyproject.toml
|
|
8
|
+
|
|
9
|
+
## [1.38.0] - 2026-07-21
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- tauri/gitignore_target, tauri/core_test_isolation: інваріант workspace-кореня замість хибної typo-версії
|
|
14
|
+
|
|
3
15
|
## [1.37.0] - 2026-07-21
|
|
4
16
|
|
|
5
17
|
### Added
|
package/package.json
CHANGED
|
@@ -15,23 +15,37 @@ rebuild (лінковка, bundling, іноді webview) замість секу
|
|
|
15
15
|
|
|
16
16
|
```
|
|
17
17
|
my-tauri-app/
|
|
18
|
-
├──
|
|
19
|
-
|
|
20
|
-
└──
|
|
21
|
-
|
|
18
|
+
├── Cargo.toml ← workspace root (virtual, без [package]): [workspace] members = ["src-tauri", "agent-core"]
|
|
19
|
+
├── src-tauri/ ← app-shell: Tauri runtime, window/AppHandle, НЕ містить LLM-залежностей
|
|
20
|
+
│ └── Cargo.toml ← [package], без [workspace] — сам по собі НЕ workspace root
|
|
21
|
+
└── agent-core/ ← agent/LLM-логіка: prompt building, tool dispatch, provider abstraction
|
|
22
|
+
├── Cargo.toml ← без залежності на tauri
|
|
22
23
|
└── tests/
|
|
23
24
|
└── fake_provider.rs ← FakeLlmProvider / MockLlmClient — без мережі
|
|
24
25
|
```
|
|
25
26
|
|
|
27
|
+
`[workspace] members = [".", "../agent-core"]` **всередині** `src-tauri/Cargo.toml` — невалідний
|
|
28
|
+
Cargo layout: `../agent-core` лежить поза деревом workspace root (`src-tauri/`), і Cargo відмовляє
|
|
29
|
+
з `workspace member '.../agent-core/Cargo.toml' is not hierarchically below the workspace root`.
|
|
30
|
+
Тому `[workspace]` живе у product-root `Cargo.toml`, що лежить НАД обома крейтами (канон
|
|
31
|
+
`rust/workspace_root.mdc` — один кореневий Cargo workspace на репозиторій).
|
|
32
|
+
|
|
33
|
+
**Наслідок:** `target/` компілюється у корінь цього workspace (`my-tauri-app/target/`), а не в
|
|
34
|
+
`src-tauri/target/` — це впливає на очікуваний запис у `.gitignore` (`tauri/gitignore_target`) і на
|
|
35
|
+
шлях у `workspaces:`-полі `Swatinem/rust-cache` GitHub Action (action бере шлях буквально як
|
|
36
|
+
`<path>/target`, тож там має стояти корінь цього workspace, не `src-tauri`).
|
|
37
|
+
|
|
26
38
|
Концерн `tauri/core_test_isolation.mjs` (read-only detector, `structural`, авто-фікс ризикований)
|
|
27
39
|
для кожного `src-tauri/Cargo.toml` перевіряє:
|
|
28
40
|
|
|
29
41
|
1. **LLM-залежність не в app-shell.** Якщо сам `src-tauri/Cargo.toml` має залежність на відомий
|
|
30
42
|
LLM SDK (напр. `async-openai`, `anthropic`, `ollama-rs`, `rig-core`, ...) — фейл: винеси
|
|
31
43
|
agent-логіку в окремий crate.
|
|
32
|
-
2. **Окремий crate не тягне tauri.**
|
|
33
|
-
|
|
34
|
-
|
|
44
|
+
2. **Окремий crate не тягне tauri.** `[workspace] members` резолвиться або з самого
|
|
45
|
+
`src-tauri/Cargo.toml` (standalone-патерн), або — коли там `[workspace]` немає — з
|
|
46
|
+
найближчого предка-workspace root (product-root `Cargo.toml` над `src-tauri/`). Якщо серед
|
|
47
|
+
members є crate з LLM-залежністю, і цей crate сам залежить від `tauri`/`tauri-*` — фейл:
|
|
48
|
+
`cargo test -p` цього крейту все одно перезбирає Tauri runtime.
|
|
35
49
|
3. **Є fake-провайдер.** Якщо такий crate є і не залежить від tauri, але ніде в його дереві
|
|
36
50
|
(`**/*.rs`) немає маркера `Fake*Llm`/`Mock*Provider`/`Stub*Client` — фейл: інтеграційні тести
|
|
37
51
|
на prompt/tool ітерації або мовчки пропускають LLM-логіку, або б'ють по реальному провайдеру.
|
|
@@ -14,6 +14,7 @@ import { parse as parseToml } from 'smol-toml'
|
|
|
14
14
|
|
|
15
15
|
import { scanGlob } from '../../../scripts/utils/glob-compat.mjs'
|
|
16
16
|
import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
|
|
17
|
+
import { findAncestorWorkspaceRoot, resolveWorkspaceMemberDirs } from '../../../scripts/utils/cargo-workspace.mjs'
|
|
17
18
|
import { getMonorepoPackageRootDirs } from '../../../scripts/lib/workspaces.mjs'
|
|
18
19
|
|
|
19
20
|
/** Стабільні reasons для трьох типів порушення. */
|
|
@@ -55,29 +56,6 @@ async function findSrcTauriDirs(cwd) {
|
|
|
55
56
|
return result
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
/**
|
|
59
|
-
* Резолвить `[workspace] members` (шляхи й прості glob-патерни) відносно `src-tauri/`
|
|
60
|
-
* у список абсолютних каталогів крейтів, що мають власний Cargo.toml.
|
|
61
|
-
* @param {string} srcTauriDir абсолютний шлях до `src-tauri/`
|
|
62
|
-
* @param {string[]} members патерни з `workspace.members`
|
|
63
|
-
* @returns {Promise<string[]>} абсолютні шляхи членів воркспейсу (без дублікатів)
|
|
64
|
-
*/
|
|
65
|
-
async function resolveWorkspaceMemberDirs(srcTauriDir, members) {
|
|
66
|
-
const found = new Set()
|
|
67
|
-
for (const pattern of members) {
|
|
68
|
-
if (pattern.includes('*')) {
|
|
69
|
-
for await (const rel of scanGlob(pattern, srcTauriDir)) {
|
|
70
|
-
const abs = resolve(srcTauriDir, rel)
|
|
71
|
-
if (existsSync(join(abs, 'Cargo.toml'))) found.add(abs)
|
|
72
|
-
}
|
|
73
|
-
continue
|
|
74
|
-
}
|
|
75
|
-
const abs = resolve(srcTauriDir, pattern)
|
|
76
|
-
if (existsSync(join(abs, 'Cargo.toml'))) found.add(abs)
|
|
77
|
-
}
|
|
78
|
-
return [...found]
|
|
79
|
-
}
|
|
80
|
-
|
|
81
59
|
/**
|
|
82
60
|
* Рекурсивно шукає в дереві крейту рядок, що відповідає `FAKE_PROVIDER_RE`
|
|
83
61
|
* (fake/mock/stub-реалізацію LLM-провайдера, зазвичай у `tests/` чи `src/`).
|
|
@@ -117,12 +95,19 @@ async function checkOneSrcTauri(srcTauriDir, cwd, reporter) {
|
|
|
117
95
|
return
|
|
118
96
|
}
|
|
119
97
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
98
|
+
// `[workspace]` живе або у самому src-tauri/Cargo.toml (старий/standalone патерн), або —
|
|
99
|
+
// канонічно (rust/workspace_root.mdc) — у предку-workspace root над src-tauri/.
|
|
100
|
+
let workspaceRootDir = srcTauriDir
|
|
101
|
+
let members = Array.isArray(parsed?.workspace?.members) ? parsed.workspace.members : []
|
|
102
|
+
if (members.length === 0) {
|
|
103
|
+
const ancestor = await findAncestorWorkspaceRoot(srcTauriDir, cwd)
|
|
104
|
+
const ancestorMembers = ancestor?.parsed?.workspace?.members
|
|
105
|
+
if (!ancestor || !Array.isArray(ancestorMembers) || ancestorMembers.length === 0) return
|
|
106
|
+
workspaceRootDir = ancestor.rootDir
|
|
107
|
+
members = ancestorMembers
|
|
123
108
|
}
|
|
124
109
|
|
|
125
|
-
const memberDirs = await resolveWorkspaceMemberDirs(
|
|
110
|
+
const memberDirs = await resolveWorkspaceMemberDirs(workspaceRootDir, members)
|
|
126
111
|
const otherMemberDirs = memberDirs.filter(d => resolve(d) !== resolve(srcTauriDir))
|
|
127
112
|
|
|
128
113
|
for (const memberDir of otherMemberDirs) {
|
|
@@ -3,9 +3,8 @@ type: JS Module
|
|
|
3
3
|
title: main.mjs
|
|
4
4
|
resource: npm/rules/tauri/gitignore_target/main.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: c9fcfc58
|
|
7
7
|
model: openai-codex/gpt-5.5
|
|
8
|
-
tier: cloud-avg
|
|
9
8
|
score: 85
|
|
10
9
|
issues: internal-name:findSrcTauriDirs,anchor-miss:(tauri.mdc),judge:inaccurate:0.98
|
|
11
10
|
judgeModel: openai-codex/gpt-5.4-mini
|
|
@@ -1,23 +1,42 @@
|
|
|
1
|
-
## `.gitignore`: ignore build-артефактів
|
|
1
|
+
## `.gitignore`: ignore build-артефактів фактичного Cargo workspace root
|
|
2
2
|
|
|
3
|
-
Кожен `<ws>/src-tauri/` (Rust-крейт Tauri) компілюється в
|
|
3
|
+
Кожен `<ws>/src-tauri/` (Rust-крейт Tauri) компілюється в `target/` свого фактичного
|
|
4
|
+
Cargo workspace root — а не завжди в `<ws>/src-tauri/target/`. Якщо над `src-tauri/` є
|
|
5
|
+
предок-workspace (канон `rust/workspace_root.mdc` — один кореневий Cargo workspace на
|
|
6
|
+
репозиторій, наприклад `<ws>/Cargo.toml` чи кореневий `Cargo.toml` монорепо), Cargo кладе
|
|
7
|
+
`target/` саме туди. Корінний `.gitignore` монорепо повинен містити точний ignore-запис
|
|
8
|
+
для кожного такого крейту, обчислений від цього фактичного workspace root:
|
|
4
9
|
|
|
5
10
|
```gitignore
|
|
6
11
|
# Tauri — Rust build artifacts (tauri.mdc)
|
|
7
|
-
<
|
|
12
|
+
<workspace-root>/target/
|
|
8
13
|
```
|
|
9
14
|
|
|
15
|
+
де `<workspace-root>` — найближчий предок `src-tauri/` з `[workspace]`, чиї `members`
|
|
16
|
+
покривають цей крейт (якщо це корінь монорепо — запис голий `target/`); якщо предка-workspace
|
|
17
|
+
немає — крейт сам собі workspace root, і запис лишається `<ws>/src-tauri/target/`.
|
|
18
|
+
|
|
10
19
|
### Чому точний шлях, а не голий `target/`
|
|
11
20
|
|
|
12
|
-
Реальний
|
|
21
|
+
Реальний прецедент: у `nitra/task` коміт `3cb0df3` помилково "виправив" коректний запис
|
|
22
|
+
`owner/target/` назад на `owner/src-tauri/target/` — автор вважав це друкарською помилкою,
|
|
23
|
+
не врахувавши, що `owner/Cargo.toml` є Cargo workspace root для `owner/src-tauri` і тому
|
|
24
|
+
`target/` справді компілюється в `owner/target/`, а не в `owner/src-tauri/target/`. Коміт
|
|
25
|
+
`ac3451e` це скасував. Тобто `<ws>/src-tauri/target/` — не універсальний дефолт, а лише
|
|
26
|
+
окремий випадок (crate без workspace root над собою); перевірка мусить рахувати очікуваний
|
|
27
|
+
шлях від реального workspace root, а не від фіксованого суфікса.
|
|
13
28
|
|
|
14
|
-
Тому перевірка звіряє **точний рядок**
|
|
29
|
+
Тому перевірка звіряє **точний рядок** обчисленого запису для кожного знайденого `src-tauri/`,
|
|
30
|
+
а не факт наявності підрядка `target/` десь у файлі — голий `target/` чи запис не того рівня
|
|
31
|
+
не закривають violation. Сторонній/мертвий каталог `target/` на диску (за старим, вже
|
|
32
|
+
невірним шляхом) також не задовольняє перевірку — вона звіряє лише вміст `.gitignore`.
|
|
15
33
|
|
|
16
34
|
### Область дії
|
|
17
35
|
|
|
18
36
|
- Обхід усіх `<ws>/src-tauri/` — спільний з `tauri/cargo_mutants_config` (`findSrcTauriDirs`, без дублювання обходу монорепо).
|
|
37
|
+
- Очікуваний шлях — `findAncestorWorkspaceRoot` (`scripts/utils/cargo-workspace.mjs`, спільно з `rust/workspace_root`).
|
|
19
38
|
- Ціль фіксу — **один** корінний `.gitignore` монорепо (не по workspace).
|
|
20
|
-
- Детектор лише **додає** відсутнє: сторонні/застарілі ignore-записи
|
|
39
|
+
- Детектор лише **додає** відсутнє: сторонні/застарілі ignore-записи не видаляються — вони можуть бути навмисними чи потребувати ручного review.
|
|
21
40
|
|
|
22
41
|
### Автофікс (T0)
|
|
23
42
|
|
|
@@ -2,15 +2,19 @@
|
|
|
2
2
|
* @see ./docs/main.md
|
|
3
3
|
*
|
|
4
4
|
* Read-only detector: для кожного знайденого `<ws>/src-tauri/` у монорепо
|
|
5
|
-
* корінний `.gitignore` повинен містити точний ignore-запис
|
|
6
|
-
*
|
|
5
|
+
* корінний `.gitignore` повинен містити точний ignore-запис для build-артефактів
|
|
6
|
+
* — але шлях залежить від фактичного Cargo workspace root крейту, не від
|
|
7
|
+
* розташування самого `src-tauri/` (tauri.mdc).
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* (
|
|
9
|
+
* Крейт `src-tauri/` компілюється в `<workspace-root>/target/`, а не обов'язково
|
|
10
|
+
* в `<ws>/src-tauri/target/`: коли над `src-tauri/` є предок-workspace (канон
|
|
11
|
+
* `rust/workspace_root` — один кореневий Cargo workspace на репозиторій), Cargo
|
|
12
|
+
* кладе `target/` саме туди. Реальний прецедент: у `nitra/task` коміт `3cb0df3`
|
|
13
|
+
* помилково "виправив" коректний запис `owner/target/` назад на `owner/src-tauri/target/`
|
|
14
|
+
* (бо `owner/Cargo.toml` — workspace root для `owner/src-tauri`), що скасував наступний
|
|
15
|
+
* коміт `ac3451e`. Тому перевірка обчислює очікуваний шлях від реального workspace root,
|
|
16
|
+
* а не від фіксованого суфікса `src-tauri/target/`, і звіряє точний рядок — голий
|
|
17
|
+
* `target/` чи запис не-того рівня не рахуються присутністю потрібного запису.
|
|
14
18
|
*
|
|
15
19
|
* Обхід `<ws>/src-tauri/` — спільний з `tauri/cargo_mutants_config`
|
|
16
20
|
* (`findSrcTauriDirs`), без дублювання.
|
|
@@ -20,6 +24,7 @@ import { readFile } from 'node:fs/promises'
|
|
|
20
24
|
import { join, relative } from 'node:path'
|
|
21
25
|
|
|
22
26
|
import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
|
|
27
|
+
import { findAncestorWorkspaceRoot } from '../../../scripts/utils/cargo-workspace.mjs'
|
|
23
28
|
import { findSrcTauriDirs } from '../cargo_mutants_config/main.mjs'
|
|
24
29
|
|
|
25
30
|
/** Стабільний reason: у корінному `.gitignore` бракує ignore-запису(ів) для `src-tauri/target/`. */
|
|
@@ -29,13 +34,22 @@ export const MISSING_GITIGNORE_TARGET_ENTRIES = 'missing-gitignore-target-entrie
|
|
|
29
34
|
export const ROOT_GITIGNORE = '.gitignore'
|
|
30
35
|
|
|
31
36
|
/**
|
|
32
|
-
* Очікуваний ignore-запис для build-артефактів одного `<ws>/src-tauri
|
|
37
|
+
* Очікуваний ignore-запис для build-артефактів одного `<ws>/src-tauri/`: шлях до
|
|
38
|
+
* `target/` фактичного Cargo workspace root цього крейту (найближчий предок з
|
|
39
|
+
* `[workspace]`, чиї `members` покривають `src-tauri/`), а не суфікс `src-tauri/target/`
|
|
40
|
+
* за замовчуванням. Якщо предка-workspace немає — крейт сам собі workspace root
|
|
41
|
+
* (standalone `src-tauri/Cargo.toml`), і `target/` лишається під `src-tauri/`.
|
|
33
42
|
* @param {string} cwd корінь монорепо
|
|
34
43
|
* @param {string} srcTauriDir абсолютний шлях до `src-tauri/`
|
|
35
|
-
* @returns {string} очікуваний рядок у корінному `.gitignore`
|
|
44
|
+
* @returns {Promise<string>} очікуваний рядок у корінному `.gitignore`
|
|
36
45
|
*/
|
|
37
|
-
export function expectedTargetEntry(cwd, srcTauriDir) {
|
|
38
|
-
|
|
46
|
+
export async function expectedTargetEntry(cwd, srcTauriDir) {
|
|
47
|
+
const ancestor = await findAncestorWorkspaceRoot(srcTauriDir, cwd)
|
|
48
|
+
if (!ancestor) {
|
|
49
|
+
return `${relative(cwd, srcTauriDir)}/target/`
|
|
50
|
+
}
|
|
51
|
+
const relRoot = relative(cwd, ancestor.rootDir)
|
|
52
|
+
return relRoot === '' ? 'target/' : `${relRoot}/target/`
|
|
39
53
|
}
|
|
40
54
|
|
|
41
55
|
/**
|
|
@@ -62,7 +76,8 @@ export async function lint(ctx) {
|
|
|
62
76
|
const srcTauriDirs = await findSrcTauriDirs(cwd)
|
|
63
77
|
if (srcTauriDirs.length === 0) return reporter.result()
|
|
64
78
|
|
|
65
|
-
const expectedEntries =
|
|
79
|
+
const expectedEntries = []
|
|
80
|
+
for (const dir of srcTauriDirs) expectedEntries.push(await expectedTargetEntry(cwd, dir))
|
|
66
81
|
const abs = join(cwd, ROOT_GITIGNORE)
|
|
67
82
|
const content = existsSync(abs) ? await readFile(abs, 'utf8') : ''
|
|
68
83
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Спільні T0 (без spawn `cargo`) утиліти для роботи з Cargo workspace-структурою:
|
|
3
|
+
* читання Cargo.toml, резолв `[workspace].members`-glob-патернів у каталоги,
|
|
4
|
+
* пошук найближчого предка-workspace root для крейту. Спільно використовується
|
|
5
|
+
* `plugins/lang-rust/rules/rust/workspace_root` і `npm/rules/tauri/gitignore_target`.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync } from 'node:fs'
|
|
8
|
+
import { readFile } from 'node:fs/promises'
|
|
9
|
+
import { dirname, join, resolve } from 'node:path'
|
|
10
|
+
|
|
11
|
+
import { parse as parseToml } from 'smol-toml'
|
|
12
|
+
|
|
13
|
+
import { scanGlob } from './glob-compat.mjs'
|
|
14
|
+
|
|
15
|
+
const TRAILING_SLASH_RE = /\/$/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Розпарсений Cargo.toml або null (файл відсутній чи невалідний TOML).
|
|
19
|
+
* @param {string} absPath абсолютний шлях до Cargo.toml
|
|
20
|
+
* @returns {Promise<Record<string, unknown>|null>} розпарсений маніфест або null
|
|
21
|
+
*/
|
|
22
|
+
export async function readCargoManifest(absPath) {
|
|
23
|
+
if (!existsSync(absPath)) return null
|
|
24
|
+
try {
|
|
25
|
+
return parseToml(await readFile(absPath, 'utf8'))
|
|
26
|
+
} catch {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Резолвить `[workspace].members`/`.exclude`-патерни (літеральні шляхи й прості
|
|
33
|
+
* glob з `*`) відносно `rootDir` у список абсолютних каталогів, що мають власний
|
|
34
|
+
* Cargo.toml. Без повної Cargo glob-семантики — лише `*`-сегменти й літерали.
|
|
35
|
+
* @param {string} rootDir корінь workspace (каталог з Cargo.toml, де живе `[workspace]`)
|
|
36
|
+
* @param {string[]} patterns патерни з `members`/`exclude`
|
|
37
|
+
* @returns {Promise<string[]>} абсолютні шляхи (без дублікатів)
|
|
38
|
+
*/
|
|
39
|
+
export async function resolveWorkspaceMemberDirs(rootDir, patterns) {
|
|
40
|
+
const found = new Set()
|
|
41
|
+
for (const pattern of patterns ?? []) {
|
|
42
|
+
const norm = pattern.replace(TRAILING_SLASH_RE, '')
|
|
43
|
+
if (norm.includes('*')) {
|
|
44
|
+
// Шаблон для `Cargo.toml` напряму (не для каталогів) — деякі раннери (Bun.Glob)
|
|
45
|
+
// за замовчуванням матчать лише файли, тож патерн на голий каталог даватиме 0 збігів.
|
|
46
|
+
for await (const relManifest of scanGlob(`${norm}/Cargo.toml`, rootDir)) {
|
|
47
|
+
found.add(resolve(rootDir, dirname(relManifest)))
|
|
48
|
+
}
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
const abs = resolve(rootDir, norm)
|
|
52
|
+
if (existsSync(join(abs, 'Cargo.toml'))) found.add(abs)
|
|
53
|
+
}
|
|
54
|
+
return [...found]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Чи покриває `[workspace].members` (мінус `.exclude`) конкретний каталог-крейт.
|
|
59
|
+
* @param {string} rootDir корінь workspace
|
|
60
|
+
* @param {string} crateDirAbs абсолютний шлях крейту
|
|
61
|
+
* @param {string[]} members `workspace.members`
|
|
62
|
+
* @param {string[]} excludes `workspace.exclude`
|
|
63
|
+
* @returns {Promise<boolean>} true — крейт є членом workspace
|
|
64
|
+
*/
|
|
65
|
+
export async function isWorkspaceMemberDir(rootDir, crateDirAbs, members, excludes) {
|
|
66
|
+
const target = resolve(crateDirAbs)
|
|
67
|
+
const memberDirs = await resolveWorkspaceMemberDirs(rootDir, members)
|
|
68
|
+
if (memberDirs.every(d => resolve(d) !== target)) return false
|
|
69
|
+
if (!excludes || excludes.length === 0) return true
|
|
70
|
+
const excludedDirs = await resolveWorkspaceMemberDirs(rootDir, excludes)
|
|
71
|
+
return excludedDirs.every(d => resolve(d) !== target)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Йде від `dirname(crateDirAbs)` вгору по предках до `repoRootAbs` (включно),
|
|
76
|
+
* шукаючи найближчий Cargo.toml з `[workspace]`, чиї `members` (мінус `exclude`)
|
|
77
|
+
* покривають `crateDirAbs`. Не перевіряє сам `crateDirAbs` (виклик для нього — окремо).
|
|
78
|
+
* @param {string} crateDirAbs абсолютний шлях крейту (напр. `<repo>/owner/src-tauri`)
|
|
79
|
+
* @param {string} repoRootAbs абсолютний корінь репозиторію (межа обходу вгору)
|
|
80
|
+
* @returns {Promise<{rootDir: string, parsed: Record<string, unknown>}|null>} найближчий ancestor workspace root або null
|
|
81
|
+
*/
|
|
82
|
+
export async function findAncestorWorkspaceRoot(crateDirAbs, repoRootAbs) {
|
|
83
|
+
const stopAt = resolve(repoRootAbs)
|
|
84
|
+
let dir = dirname(resolve(crateDirAbs))
|
|
85
|
+
for (;;) {
|
|
86
|
+
const parsed = await readCargoManifest(join(dir, 'Cargo.toml'))
|
|
87
|
+
if (parsed?.workspace) {
|
|
88
|
+
const members = Array.isArray(parsed.workspace.members) ? parsed.workspace.members : []
|
|
89
|
+
const excludes = Array.isArray(parsed.workspace.exclude) ? parsed.workspace.exclude : []
|
|
90
|
+
if (await isWorkspaceMemberDir(dir, crateDirAbs, members, excludes)) {
|
|
91
|
+
return { rootDir: dir, parsed }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (dir === stopAt) return null
|
|
95
|
+
const parent = dirname(dir)
|
|
96
|
+
if (parent === dir) return null
|
|
97
|
+
dir = parent
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: cargo-workspace.mjs
|
|
4
|
+
resource: npm/scripts/utils/cargo-workspace.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: c852e1fe
|
|
7
|
+
model: openai-codex/gpt-5.4-mini
|
|
8
|
+
tier: cloud-min
|
|
9
|
+
score: 100
|
|
10
|
+
issues: judge:inaccurate:0.98
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Огляд
|
|
15
|
+
|
|
16
|
+
Спільні утиліти для читання `Cargo.toml`, розгортання `[workspace].members`-glob-патернів у каталоги та пошуку найближчого предка-workspace root для крейта. Публічні `readCargoManifest`, `resolveWorkspaceMemberDirs`, `isWorkspaceMemberDir`, `findAncestorWorkspaceRoot` працюють без запуску `cargo`, перехоплюють помилки й у частині випадків повертають порожнє значення замість винятку, щоб інші правила могли безпечно перевіряти належність каталогу до workspace або знаходити workspace root.
|
|
17
|
+
|
|
18
|
+
## Поведінка
|
|
19
|
+
|
|
20
|
+
- **readCargoManifest** — читає `Cargo.toml` і повертає розпарсений manifest, або `null`, якщо файл відсутній чи TOML невалідний.
|
|
21
|
+
- **resolveWorkspaceMemberDirs** — перетворює workspace-патерни на список абсолютних каталогів із власним `Cargo.toml`, усуваючи дублікати.
|
|
22
|
+
- **isWorkspaceMemberDir** — визначає, чи належить конкретний каталог до workspace з урахуванням `members` і `exclude`.
|
|
23
|
+
- **findAncestorWorkspaceRoot** — шукає найближчий предок `Cargo.toml` з `[workspace]`, чий workspace покриває крейт; сам каталог крейту не перевіряє.
|
|
24
|
+
|
|
25
|
+
## Публічний API
|
|
26
|
+
|
|
27
|
+
- readCargoManifest — Розпарсений Cargo.toml або null (файл відсутній чи невалідний TOML).
|
|
28
|
+
- resolveWorkspaceMemberDirs — Резолвить `[workspace].members`/`.exclude`-патерни (літеральні шляхи й прості
|
|
29
|
+
glob з `*`) відносно `rootDir` у список абсолютних каталогів, що мають власний
|
|
30
|
+
Cargo.toml. Без повної Cargo glob-семантики — лише `*`-сегменти й літерали.
|
|
31
|
+
- isWorkspaceMemberDir — Чи покриває `[workspace].members` (мінус `.exclude`) конкретний каталог-крейт.
|
|
32
|
+
- findAncestorWorkspaceRoot — Йде від `dirname(crateDirAbs)` вгору по предках до `repoRootAbs` (включно),
|
|
33
|
+
шукаючи найближчий Cargo.toml з `[workspace]`, чиї `members` (мінус `exclude`)
|
|
34
|
+
покривають `crateDirAbs`. Не перевіряє сам `crateDirAbs` (виклик для нього — окремо).
|
|
35
|
+
|
|
36
|
+
## Гарантії поведінки
|
|
37
|
+
|
|
38
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
39
|
+
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
40
|
+
- За певних помилок повертає порожнє значення (напр. `null`) замість винятку.
|
|
@@ -9,6 +9,7 @@ resource: npm/scripts/utils/
|
|
|
9
9
|
| [apply-to-files.mjs](apply-to-files.md) | JS Module |
|
|
10
10
|
| [ast-extract.mjs](ast-extract.md) | JS Module |
|
|
11
11
|
| [ast-scan-utils.mjs](ast-scan-utils.md) | JS Module |
|
|
12
|
+
| [cargo-workspace.mjs](cargo-workspace.md) | JS Module |
|
|
12
13
|
| [ensure-gitignore-entries.mjs](ensure-gitignore-entries.md) | JS Module |
|
|
13
14
|
| [find-package-json-paths.mjs](find-package-json-paths.md) | JS Module |
|
|
14
15
|
| [glob-compat.mjs](glob-compat.md) | JS Module |
|
|
@@ -19,6 +20,7 @@ resource: npm/scripts/utils/
|
|
|
19
20
|
| [resolve-js-root.mjs](resolve-js-root.md) | JS Module |
|
|
20
21
|
| [spawn-async.mjs](spawn-async.md) | JS Module |
|
|
21
22
|
| [test-helpers.mjs](test-helpers.md) | JS Module |
|
|
23
|
+
| [uv-workspace.mjs](uv-workspace.md) | JS Module |
|
|
22
24
|
| [walk-cache.mjs](walk-cache.md) | JS Module |
|
|
23
25
|
| [walkDir.mjs](walkDir.md) | JS Module |
|
|
24
26
|
| [with-lock.mjs](with-lock.md) | JS Module |
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: uv-workspace.mjs
|
|
4
|
+
resource: npm/scripts/utils/uv-workspace.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: b7602da3
|
|
7
|
+
model: openai-codex/gpt-5.4-mini
|
|
8
|
+
tier: cloud-min
|
|
9
|
+
score: 100
|
|
10
|
+
issues: judge:inaccurate:0.98
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Огляд
|
|
15
|
+
|
|
16
|
+
`readPyprojectManifest` читає `pyproject.toml` і повертає маніфест uv workspace, а `resolveUvWorkspaceMemberDirs` резолвить каталоги з `[tool.uv.workspace].members` та `.exclude` за тією самою glob-семантикою, що й у Cargo workspaces. Це read-only, fail-safe набір спільних T0-утиліт: він не пише у ФС чи БД, не кидає винятків назовні та для частини помилок повертає порожнє значення замість помилки.
|
|
17
|
+
|
|
18
|
+
## Поведінка
|
|
19
|
+
|
|
20
|
+
- `readPyprojectManifest` — читає `pyproject.toml` для uv workspace і повертає розпарсений manifest або `null`, якщо файл відсутній чи TOML невалідний.
|
|
21
|
+
- `resolveUvWorkspaceMemberDirs` — перетворює `members` і `exclude` uv workspace на список абсолютних каталогів із власним `pyproject.toml`, враховуючи літеральні шляхи та прості glob-патерни.
|
|
22
|
+
|
|
23
|
+
## Публічний API
|
|
24
|
+
|
|
25
|
+
- readPyprojectManifest — Розпарсений pyproject.toml або null (файл відсутній чи невалідний TOML).
|
|
26
|
+
- resolveUvWorkspaceMemberDirs — Резолвить `[tool.uv.workspace].members`/`.exclude`-патерни (літеральні шляхи й прості
|
|
27
|
+
glob з `*`) відносно `rootDir` у список абсолютних каталогів, що мають власний
|
|
28
|
+
pyproject.toml. Без повної glob-семантики uv — лише `*`-сегменти й літерали.
|
|
29
|
+
|
|
30
|
+
## Гарантії поведінки
|
|
31
|
+
|
|
32
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
33
|
+
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
34
|
+
- За певних помилок повертає порожнє значення (напр. `null`) замість винятку.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Спільні T0 (без spawn `uv`) утиліти для роботи з uv workspace-структурою: читання
|
|
3
|
+
* `pyproject.toml`, резолв `[tool.uv.workspace].members`/`.exclude`-glob-патернів у
|
|
4
|
+
* каталоги. Дзеркалить `cargo-workspace.mjs` (rust/workspace_root) — uv workspaces
|
|
5
|
+
* навмисно змодельовані на Cargo workspaces, glob-семантика `members`/`exclude` та сама.
|
|
6
|
+
* Не імпортується напряму з rust-боку (окремі плагіни) — дублювання свідоме.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync } from 'node:fs'
|
|
9
|
+
import { readFile } from 'node:fs/promises'
|
|
10
|
+
import { dirname, resolve } from 'node:path'
|
|
11
|
+
|
|
12
|
+
import { parse as parseToml } from 'smol-toml'
|
|
13
|
+
|
|
14
|
+
import { scanGlob } from './glob-compat.mjs'
|
|
15
|
+
|
|
16
|
+
const TRAILING_SLASH_RE = /\/$/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Розпарсений pyproject.toml або null (файл відсутній чи невалідний TOML).
|
|
20
|
+
* @param {string} absPath абсолютний шлях до pyproject.toml
|
|
21
|
+
* @returns {Promise<Record<string, unknown>|null>} розпарсений маніфест або null
|
|
22
|
+
*/
|
|
23
|
+
export async function readPyprojectManifest(absPath) {
|
|
24
|
+
if (!existsSync(absPath)) return null
|
|
25
|
+
try {
|
|
26
|
+
return parseToml(await readFile(absPath, 'utf8'))
|
|
27
|
+
} catch {
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Резолвить `[tool.uv.workspace].members`/`.exclude`-патерни (літеральні шляхи й прості
|
|
34
|
+
* glob з `*`) відносно `rootDir` у список абсолютних каталогів, що мають власний
|
|
35
|
+
* pyproject.toml. Без повної glob-семантики uv — лише `*`-сегменти й літерали.
|
|
36
|
+
* @param {string} rootDir корінь workspace (каталог з pyproject.toml, де живе `[tool.uv.workspace]`)
|
|
37
|
+
* @param {string[]} patterns патерни з `members`/`exclude`
|
|
38
|
+
* @returns {Promise<string[]>} абсолютні шляхи (без дублікатів)
|
|
39
|
+
*/
|
|
40
|
+
export async function resolveUvWorkspaceMemberDirs(rootDir, patterns) {
|
|
41
|
+
const found = new Set()
|
|
42
|
+
for (const pattern of patterns ?? []) {
|
|
43
|
+
const norm = pattern.replace(TRAILING_SLASH_RE, '')
|
|
44
|
+
if (norm.includes('*')) {
|
|
45
|
+
// Патерн для pyproject.toml напряму (не для каталогів) — деякі раннери (Bun.Glob)
|
|
46
|
+
// за замовчуванням матчать лише файли, тож патерн на голий каталог даватиме 0 збігів.
|
|
47
|
+
for await (const relManifest of scanGlob(`${norm}/pyproject.toml`, rootDir)) {
|
|
48
|
+
found.add(resolve(rootDir, dirname(relManifest)))
|
|
49
|
+
}
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
const abs = resolve(rootDir, norm)
|
|
53
|
+
if (existsSync(`${abs}/pyproject.toml`)) found.add(abs)
|
|
54
|
+
}
|
|
55
|
+
return [...found]
|
|
56
|
+
}
|