@mirta/workspace 0.0.1 → 0.4.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/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ This is free and unencumbered software released into the public domain.
2
+
3
+ Anyone is free to copy, modify, publish, use, compile, sell, or
4
+ distribute this software, either in source code form or as a compiled
5
+ binary, for any purpose, commercial or non-commercial, and by any
6
+ means.
7
+
8
+ In jurisdictions that recognize copyright laws, the author or authors
9
+ of this software dedicate any and all copyright interest in the
10
+ software to the public domain. We make this dedication for the benefit
11
+ of the public at large and to the detriment of our heirs and
12
+ successors. We intend this dedication to be an overt act of
13
+ relinquishment in perpetuity of all present and future rights to this
14
+ software under copyright law.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ For more information, please refer to <https://unlicense.org>
package/README.md CHANGED
@@ -1,45 +1,109 @@
1
- # @mirta/workspace
1
+ # `@mirta/workspace`
2
2
 
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
3
+ [![en](https://img.shields.io/badge/lang-en-olivedrab.svg?style=flat-square)](https://github.com/wb-mirta/core/blob/latest/packages/mirta-workspace/README.md)
4
+ [![ru](https://img.shields.io/badge/lang-ru-dimgray.svg?style=flat-square)](https://github.com/wb-mirta/core/blob/latest/packages/mirta-workspace/README.ru.md)
5
+ [![NPM Version](https://img.shields.io/npm/v/@mirta/workspace?style=flat-square)](https://npmjs.com/package/@mirta/workspace)
6
+ [![NPM Downloads](https://img.shields.io/npm/dm/@mirta/workspace?style=flat-square&logo=npm)](https://npmjs.com/package/@mirta/workspace)
4
7
 
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
8
+ > Utility set for analyzing repository structure in projects based on Mirta framework.
6
9
 
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
10
+ `@mirta/workspace` helps tools determine:
11
+ - Where is the project root?
12
+ - Which package manager is used?
13
+ - Which packages are declared in `workspaces`?
8
14
 
9
- ## Purpose
15
+ Designed for reuse in other packages without heavy dependencies (e.g., from Rollup).<br/>
10
16
 
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@mirta/workspace`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
17
+ **Not intended for execution in the Duktape environment on Wiren Board controllers.**
15
18
 
16
- ## What is OIDC Trusted Publishing?
19
+ ## 📦 Installation
17
20
 
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
21
+ ```bash
22
+ # Not required directly — used internally by Mirta
23
+ pnpm add -D @mirta/workspace
24
+ ```
25
+ ⚠️ This package is part of Mirta's internal infrastructure. It is typically not used directly.
19
26
 
20
- ## Setup Instructions
27
+ ## 🚀 Quick Start
21
28
 
22
- To properly configure OIDC trusted publishing for this package:
29
+ ```ts
30
+ import { resolveMonorepoContextAsync } from '@mirta/workspace'
23
31
 
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
32
+ const context = await resolveMonorepoContextAsync(process.cwd())
28
33
 
29
- ## DO NOT USE THIS PACKAGE
34
+ console.log(context.rootDir) // /home/user/my-mirta-repo
35
+ console.log(context.manager) // 'pnpm'
36
+ console.log(context.packages) // [{ name: '@mirta/core', workspacePath: 'packages/core' }, ...]
37
+ ```
38
+ ## 🧰 API
30
39
 
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
40
+ `resolveWorkspaceContextAsync(cwd: string): Promise<WorkspaceContext>`<br/>
41
+ Asynchronously resolves the workspace context (lightweight variant).
36
42
 
37
- ## More Information
43
+ Finds the workspace root by the presence of a lockfile (`pnpm-lock.yaml`, `yarn.lock`, etc.) and reads `package.json`.
38
44
 
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
45
+ Use this function when you only need to identify the workspace root and the package manager in use, without resolving all packages.
46
+
47
+ Returns:
48
+
49
+ ```ts
50
+ interface WorkspaceContext {
51
+ rootDir: string // Root directory (where lockfile is located)
52
+ manager: PackageManager // 'pnpm' | 'bun' | 'yarn' | 'npm'
53
+ workspaces?: string[] // Array of glob patterns from the `workspaces` field
54
+ }
55
+ ```
56
+ Throws: `WorkspaceError` if:
57
+
58
+ Lockfile not found (`noLockfile`)
59
+ `workspaces` has invalid format (`badWorkspacesFormat`)
60
+
61
+ ---
62
+
63
+ `resolveMonorepoContextAsync(cwd: string): Promise<MonorepoContext>`<br/>
64
+ Asynchronously resolves the full monorepo context.
65
+
66
+ Based on `WorkspaceContext`, finds all packages declared in workspaces and reads their `package.json` using `@mirta/package`.
67
+
68
+ Returns:
69
+
70
+ ```ts
71
+ interface MonorepoContext {
72
+ rootDir: string
73
+ manager: PackageManager
74
+ packages: readonly PackageDefinition[]
75
+ }
76
+ ```
77
+ Packages are returned sorted by path length (longer paths first) to ensure correct matching in the future.
78
+
79
+ Results are cached by `rootDir` for performance.
42
80
 
43
81
  ---
44
82
 
45
- **Maintained for OIDC setup purposes only**
83
+ `toPosix(path: string): string`<br/>
84
+ Converts a path to POSIX format (with `/`), even on Windows.
85
+
86
+ Used to normalize paths before comparison and processing.
87
+
88
+ ## 🧩 Supported Package Managers
89
+
90
+ Detected by lockfiles:
91
+ - `pnpm` → `pnpm-lock.yaml`
92
+ - `yarn` → `yarn.lock`
93
+ - `npm` → `package-lock.json`
94
+ - `bun` → `bun.lock`
95
+
96
+ ## ✅ Testing
97
+
98
+ The package is fully covered with unit tests:
99
+ - Finding root by lockfile
100
+ - Handling `workspaces`
101
+ - Collecting packages, sorting, caching
102
+ - Cross-platform compatibility (Windows/POSIX)
103
+
104
+ Uses Vitest and mocked dependencies (`find-up`, `glob`, `@mirta/package`).
105
+
106
+ ## ⚠️ Limitations
107
+
108
+ **Works only in Node.js** (not in Duktape).<br/>
109
+ `workspaces` must be an array of strings.
package/README.ru.md ADDED
@@ -0,0 +1,109 @@
1
+ # `@mirta/workspace`
2
+
3
+ [![en](https://img.shields.io/badge/lang-en-dimgray.svg?style=flat-square)](https://github.com/wb-mirta/core/blob/latest/packages/mirta-workspace/README.md)
4
+ [![ru](https://img.shields.io/badge/lang-ru-olivedrab.svg?style=flat-square)](https://github.com/wb-mirta/core/blob/latest/packages/mirta-workspace/README.ru.md)
5
+ [![NPM Version](https://img.shields.io/npm/v/@mirta/workspace?style=flat-square)](https://npmjs.com/package/@mirta/workspace)
6
+ [![NPM Downloads](https://img.shields.io/npm/dm/@mirta/workspace?style=flat-square&logo=npm)](https://npmjs.com/package/@mirta/workspace)
7
+
8
+ > Утилиты для анализа структуры репозитория в проектах на базе фреймворка Mirta.
9
+
10
+ `@mirta/workspace` помогает инструментам определить:
11
+ - Где находится корень проекта?
12
+ - Какой пакетный менеджер используется?
13
+ - Какие пакеты объявлены в `workspaces`?
14
+
15
+ Предназначен для переиспользования в других пакетах без тяжёлых зависимостей (например, от Rollup).<br/>
16
+
17
+ **Не предназначен для выполнения в среде Duktape на контроллерах Wiren Board.**
18
+
19
+ ---
20
+
21
+ ## 📦 Установка
22
+
23
+ ```bash
24
+ # Не требуется напрямую — используется внутри Mirta
25
+ pnpm add -D @mirta/workspace
26
+ ```
27
+ ⚠️ Этот пакет — часть внутренней инфраструктуры фреймворка Mirta. Обычно он не используется напрямую.
28
+
29
+ ## 🚀 Быстрый старт
30
+
31
+ ```ts
32
+ import { resolveMonorepoContextAsync } from '@mirta/workspace'
33
+
34
+ const context = await resolveMonorepoContextAsync(process.cwd())
35
+
36
+ console.log(context.rootDir) // /home/user/my-mirta-repo
37
+ console.log(context.manager) // 'pnpm'
38
+ console.log(context.packages) // [{ name: '@mirta/core', workspacePath: 'packages/core' }, ...]
39
+ ```
40
+ ## 🧰 API
41
+
42
+ `resolveWorkspaceContextAsync(cwd: string): Promise<WorkspaceContext>`<br/>
43
+ Асинхронно определяет контекст рабочей области.
44
+
45
+ Находит корень проекта по наличию lock-файла (`pnpm-lock.yaml`, `yarn.lock` и др.) и читает `package.json`.
46
+
47
+ Возвращает:
48
+
49
+ ```ts
50
+ interface WorkspaceContext {
51
+ rootDir: string // Корневая директория (где находится lock-файл)
52
+ manager: PackageManager // 'pnpm' | 'yarn' | 'bun' | 'npm'
53
+ workspaces?: string[] // Массив glob-паттернов из поля `workspaces`
54
+ }
55
+ ```
56
+ Выбрасывает: `WorkspaceError`, если:
57
+
58
+ Не найден lock-файл (`noLockfile`)<br/>
59
+ `workspaces` имеет недопустимый формат (`badWorkspacesFormat`)
60
+
61
+ ---
62
+
63
+ `resolveMonorepoContextAsync(cwd: string): Promise<MonorepoContext>`<br/>
64
+ Асинхронно определяет полный контекст монорепозитория.
65
+
66
+ На основе `WorkspaceContext` находит все пакеты, объявленные в `workspaces`, и читает их `package.json` с помощью `@mirta/package`.
67
+
68
+ Возвращает:
69
+
70
+ ```ts
71
+ interface MonorepoContext {
72
+ rootDir: string
73
+ manager: PackageManager
74
+ packages: readonly PackageDefinition[]
75
+ }
76
+ ```
77
+ Пакеты возвращаются отсортированными по длине пути (сначала — более вложенные), чтобы обеспечить корректность сопоставления в будущем.
78
+
79
+ Результат кэшируется по `rootDir` для производительности.
80
+
81
+ ---
82
+
83
+ `toPosix(path: string): string`
84
+ Приводит путь к POSIX-формату (с `/`), даже на Windows.
85
+
86
+ Используется для нормализации путей перед сравнением и обработкой.
87
+
88
+ ## 🧩 Поддерживаемые пакетные менеджеры
89
+
90
+ Определяются по lock-файлам:
91
+ - `pnpm` → `pnpm-lock.yaml`
92
+ - `yarn` → `yarn.lock`
93
+ - `npm` → `package-lock.json`
94
+ - `bun` → `bun.lock`
95
+
96
+ ## ✅ Тестирование
97
+
98
+ Пакет полностью покрыт юнит-тестами:
99
+ - Поиск корня по lock-файлу
100
+ - Обработка `workspaces`
101
+ - Сбор пакетов, сортировка, кэширование
102
+ - Кроссплатформенность (Windows/POSIX)
103
+
104
+ Используется Vitest, моки зависимостей (`find-up`, `glob`, `@mirta/package`).
105
+
106
+ ## ⚠️ Ограничения
107
+
108
+ **Работает только в Node.js** (не в Duktape).<br/>
109
+ workspaces должно быть массивом строк.
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Тип пакетного менеджера.
3
+ *
4
+ * @since 0.4.0
5
+ *
6
+ **/
7
+ type PackageManager = 'pnpm' | 'yarn' | 'bun' | 'npm';
8
+ /**
9
+ * Описывает контекст рабочей области (workspace).
10
+ *
11
+ * Содержит информацию о корневой директории, используемом пакетном менеджере
12
+ * и объявленных рабочих пространствах (workspaces).
13
+ *
14
+ * @since 0.4.0
15
+ *
16
+ **/
17
+ interface WorkspaceContext {
18
+ /**
19
+ * Абсолютный путь к корневой директории репозитория в формате POSIX.
20
+ *
21
+ **/
22
+ readonly rootDir: string;
23
+ /**
24
+ * Определённый пакетный менеджер, используемый в проекте.
25
+ **/
26
+ readonly manager: PackageManager;
27
+ /**
28
+ * Необязательный массив glob-паттернов, определяющих пути к пакетам в монорепозитории.
29
+ *
30
+ * Соответствует полю `workspaces` в `package.json`.
31
+ * Должен быть массивом строк, например: `["packages/*"]`.
32
+ *
33
+ **/
34
+ readonly workspaces?: readonly string[];
35
+ }
36
+ /**
37
+ * Асинхронно определяет контекст рабочей области (workspace), начиная с заданной директории.
38
+ *
39
+ * Находит корень проекта по наличию lock-файла (pnpm/yarn/npm/bun), читает `package.json`
40
+ * и извлекает информацию о пакетном менеджере и рабочих пространствах.
41
+ *
42
+ * @param cwd - Рабочая директория, с которой начинается поиск.
43
+ * @returns Объект {@link WorkspaceContext} с корнем, менеджером и полем `workspaces` (если есть).
44
+ * @throws {WorkspaceError} Если lock-файл не найден или `workspaces` имеет недопустимый формат.
45
+ * @throws {PackageError} Если `package.json` отсутствует, недоступен или содержит невалидный JSON.
46
+ *
47
+ * @remarks
48
+ * - Поле `workspaces` может отсутствовать — это не ошибка.
49
+ * - Все пути возвращаются в POSIX-формате.
50
+ *
51
+ * @since 0.4.0
52
+ *
53
+ **/
54
+ declare function resolveWorkspaceContextAsync(cwd: string): Promise<WorkspaceContext>;
55
+
56
+ /**
57
+ * Контекст монорепозитория, содержащий корневую директорию и список пакетов.
58
+ *
59
+ * Является результатом анализа структуры монорепы.
60
+ *
61
+ * @since 0.4.0
62
+ *
63
+ **/
64
+ interface MonorepoContext {
65
+ /**
66
+ * Абсолютный путь к корневой директории монорепозитория в формате POSIX.
67
+ *
68
+ **/
69
+ readonly rootDir: string;
70
+ /**
71
+ * Определённый пакетный менеджер, используемый в проекте.
72
+ *
73
+ **/
74
+ readonly manager: PackageManager;
75
+ /**
76
+ * Список всех пакетов, объявленных в рабочих пространствах.
77
+ * Отсортирован по длине пути (от самых вложенных).
78
+ *
79
+ **/
80
+ readonly packages: readonly PackageDefinition[];
81
+ }
82
+ /**
83
+ * Описывает структуру пакета в монорепозитории.
84
+ *
85
+ * Содержит имя пакета и его путь относительно корня монорепы.
86
+ *
87
+ * @since 0.4.0
88
+ *
89
+ **/
90
+ interface PackageDefinition {
91
+ /**
92
+ * Имя пакета, указанное в `package.json`.
93
+ *
94
+ **/
95
+ readonly name: string;
96
+ readonly version?: string;
97
+ /**
98
+ * Признак того, что пакет не предназначен для публикации.
99
+ *
100
+ * Если значение `true`, пакет нельзя опубликовать в реестре (например, npm).
101
+ * Используется для защиты от случайной публикации внутренних или служебных пакетов.
102
+ *
103
+ **/
104
+ readonly isPrivate: boolean;
105
+ /**
106
+ * Путь к пакету относительно корня монорепозитория.
107
+ * Используется для сопоставления чанков с пакетами.
108
+ *
109
+ **/
110
+ readonly workspacePath: string;
111
+ }
112
+ /**
113
+ * Асинхронно разрешает контекст монорепозитория.
114
+ *
115
+ * Находит корень проекта, пакетный менеджер и все пакеты, объявленные в `workspaces`.
116
+ *
117
+ * @param cwd - Рабочая директория, с которой начинается поиск.
118
+ * @returns Объект {@link MonorepoContext} с информацией о монорепе.
119
+ * @throws {WorkspaceError} Если корень не найден или какой-либо из пакетов не имеет имени.
120
+ * @throws {PackageError} Если `package.json` недоступен или содержит невалидный JSON.
121
+ *
122
+ * @remarks
123
+ * - Требуется наличие lock-файла и поля `workspaces` в корневом `package.json`.
124
+ * - Все пути возвращаются в POSIX-формате.
125
+ *
126
+ * @since 0.4.0
127
+ *
128
+ **/
129
+ declare function resolveMonorepoContextAsync(cwd: string): Promise<MonorepoContext>;
130
+
131
+ export { resolveMonorepoContextAsync, resolveWorkspaceContextAsync };
132
+ export type { MonorepoContext, PackageDefinition, PackageManager, WorkspaceContext };
package/dist/index.mjs ADDED
@@ -0,0 +1,217 @@
1
+ import nodePath from 'node:path';
2
+ import { findUp } from 'find-up';
3
+ import { toPosix, readPackageAsync } from '@mirta/package';
4
+ import { glob } from 'node:fs/promises';
5
+
6
+ /**
7
+ * Класс ошибки для обработки проблем с монорепозиторием,
8
+ * расширяющий стандартный Error.
9
+ *
10
+ * @since 0.4.0
11
+ *
12
+ **/
13
+ class WorkspaceError extends Error {
14
+ /** Код ошибки для программной идентификации. */
15
+ code;
16
+ /**
17
+ * Приватный конструктор для создания экземпляра ошибки.
18
+ *
19
+ * @param message - Сообщение об ошибке
20
+ * @param code - Код ошибки
21
+ * @param scope - Область, к которой относится ошибка (по умолчанию '@mirta/workspace').
22
+ *
23
+ **/
24
+ constructor(message, code, scope = '@mirta/workspace') {
25
+ super(`[${scope}] ${message}`);
26
+ this.name = 'WorkspaceError';
27
+ this.code = code;
28
+ if ('captureStackTrace' in Error)
29
+ // eslint-disable-next-line @typescript-eslint/unbound-method
30
+ Error.captureStackTrace(this, WorkspaceError.get);
31
+ }
32
+ /** Карта кодов ошибок с соответствующими сообщениями. */
33
+ static codeMappings = {
34
+ /**
35
+ * Ошибка, возникающая, когда не найден ни один из lock-файлов пакетных менеджеров
36
+ * (pnpm-lock.yaml, yarn.lock, package-lock.json, bun.lockb) в текущей или родительских директориях.
37
+ *
38
+ **/
39
+ noLockfile: () => 'No lockfile (pnpm/yarn/bun/npm) found. Required to detect workspace root',
40
+ noPackageName: (packagePath) => `Package with path "${packagePath}" missing required 'name' field in package.json`,
41
+ noWorkspaces: () => 'No workspaces configured in root package.json',
42
+ invalidWorkspaces: (pkgPath) => `Invalid workspaces in "${pkgPath}": must be array of strings`,
43
+ };
44
+ /**
45
+ * Статический метод для получения экземпляра ошибки по коду.
46
+ *
47
+ * @template T - Тип ключа из codeMappings
48
+ * @param code - Код ошибки
49
+ * @param args - Аргументы для формирования сообщения
50
+ * @returns Экземпляр {@link WorkspaceError}
51
+ *
52
+ **/
53
+ static get(code, ...args) {
54
+ const messageFn = this.codeMappings[code];
55
+ const message = messageFn(...args);
56
+ return new WorkspaceError(message, code);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Сопоставление lock-файлов с пакетными менеджерами.
62
+ *
63
+ * @since 0.4.0
64
+ *
65
+ **/
66
+ const lockFileMappings = {
67
+ 'pnpm-lock.yaml': 'pnpm',
68
+ 'yarn.lock': 'yarn',
69
+ 'bun.lock': 'bun',
70
+ 'package-lock.json': 'npm',
71
+ };
72
+ /**
73
+ * Проверяет, что поле `workspaces` в `package.json` имеет корректный формат.
74
+ * Должно быть массивом строк или отсутствовать. Использование объекта (например, `{ packages: [...] }`) недопустимо.
75
+ *
76
+ * @param workspaces - Значение поля `workspaces` для проверки.
77
+ * @param pkgPath - Путь к package.json (используется в сообщении об ошибке).
78
+ *
79
+ * @throws {WorkspaceError} Если формат неверен.
80
+ *
81
+ * @since 0.4.0
82
+ *
83
+ **/
84
+ function assertWorkspacesFieldFormat(workspaces, pkgPath) {
85
+ if (workspaces === null || workspaces === undefined)
86
+ return;
87
+ if (Array.isArray(workspaces) && workspaces.every(item => typeof item === 'string'))
88
+ return;
89
+ throw WorkspaceError.get('invalidWorkspaces', pkgPath.replaceAll(nodePath.win32.sep, nodePath.posix.sep));
90
+ }
91
+ /**
92
+ * Асинхронно определяет контекст рабочей области (workspace), начиная с заданной директории.
93
+ *
94
+ * Находит корень проекта по наличию lock-файла (pnpm/yarn/npm/bun), читает `package.json`
95
+ * и извлекает информацию о пакетном менеджере и рабочих пространствах.
96
+ *
97
+ * @param cwd - Рабочая директория, с которой начинается поиск.
98
+ * @returns Объект {@link WorkspaceContext} с корнем, менеджером и полем `workspaces` (если есть).
99
+ * @throws {WorkspaceError} Если lock-файл не найден или `workspaces` имеет недопустимый формат.
100
+ * @throws {PackageError} Если `package.json` отсутствует, недоступен или содержит невалидный JSON.
101
+ *
102
+ * @remarks
103
+ * - Поле `workspaces` может отсутствовать — это не ошибка.
104
+ * - Все пути возвращаются в POSIX-формате.
105
+ *
106
+ * @since 0.4.0
107
+ *
108
+ **/
109
+ async function resolveWorkspaceContextAsync(cwd) {
110
+ const lockFiles = Object.keys(lockFileMappings);
111
+ const lockFilePath = toPosix(await findUp(lockFiles, { cwd }));
112
+ if (!lockFilePath)
113
+ throw WorkspaceError.get('noLockfile');
114
+ const rootDir = nodePath.dirname(lockFilePath);
115
+ const pkgPath = `${rootDir}/package.json`;
116
+ const pkg = await readPackageAsync(pkgPath);
117
+ assertWorkspacesFieldFormat(pkg.workspaces, pkgPath);
118
+ const fileName = nodePath.basename(lockFilePath);
119
+ return {
120
+ rootDir,
121
+ manager: lockFileMappings[fileName],
122
+ workspaces: pkg.workspaces,
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Кэш пакетов монорепозитория по корневой директории.
128
+ * Позволяет избежать повторного сканирования файловой системы.
129
+ *
130
+ * @since 0.4.0
131
+ *
132
+ **/
133
+ const packagesCache = new Map();
134
+ /**
135
+ * Асинхронно разрешает контекст монорепозитория.
136
+ *
137
+ * Находит корень проекта, пакетный менеджер и все пакеты, объявленные в `workspaces`.
138
+ *
139
+ * @param cwd - Рабочая директория, с которой начинается поиск.
140
+ * @returns Объект {@link MonorepoContext} с информацией о монорепе.
141
+ * @throws {WorkspaceError} Если корень не найден или какой-либо из пакетов не имеет имени.
142
+ * @throws {PackageError} Если `package.json` недоступен или содержит невалидный JSON.
143
+ *
144
+ * @remarks
145
+ * - Требуется наличие lock-файла и поля `workspaces` в корневом `package.json`.
146
+ * - Все пути возвращаются в POSIX-формате.
147
+ *
148
+ * @since 0.4.0
149
+ *
150
+ **/
151
+ async function resolveMonorepoContextAsync(cwd) {
152
+ const context = await resolveWorkspaceContextAsync(cwd);
153
+ const packages = await resolveMonorepoPackagesAsync(context);
154
+ return {
155
+ rootDir: context.rootDir,
156
+ manager: context.manager,
157
+ packages,
158
+ };
159
+ }
160
+ /**
161
+ * Асинхронно разрешает список пакетов монорепы.
162
+ *
163
+ * Находит все `package.json` в директориях, указанных в `workspaces`.
164
+ *
165
+ * @param context - Контекст workspace.
166
+ * @returns Массив пакетов, отсортированный по длине пути (от самых вложенных).
167
+ * @throws {WorkspaceError} Если пакет не имеет имени.
168
+ *
169
+ * @remarks
170
+ * - Результат кэшируется по `rootDir` для производительности.
171
+ * - Сортировка нужна, чтобы при сопоставлении по пути сначала проверялись более специфичные пакеты.
172
+ *
173
+ * @since 0.4.0
174
+ *
175
+ **/
176
+ async function resolveMonorepoPackagesAsync(context) {
177
+ const { rootDir, workspaces = [] } = context;
178
+ const cachedPackages = packagesCache.get(rootDir);
179
+ // 1. Проверяем кэш
180
+ if (cachedPackages !== undefined)
181
+ return cachedPackages;
182
+ // 2. Пробуем найти пакеты, фиксируясь на обязательном наличии `package.json`
183
+ const pkgPatterns = workspaces.map(w => `${w}/package.json`);
184
+ const packages = [];
185
+ for await (const rawPkgPath of glob(pkgPatterns, {
186
+ cwd: rootDir,
187
+ exclude: ['node_modules/**'],
188
+ })) {
189
+ const pkgPath = toPosix(rawPkgPath);
190
+ const pkg = await readPackageAsync(`${rootDir}/${pkgPath}`);
191
+ if (!pkg.name)
192
+ throw WorkspaceError.get('noPackageName', pkgPath);
193
+ packages.push({
194
+ name: pkg.name,
195
+ version: pkg.version,
196
+ isPrivate: pkg.private === true,
197
+ workspacePath: nodePath.dirname(pkgPath),
198
+ });
199
+ }
200
+ // Сортируем по убыванию длины пути, чтобы сначала шли более вложенные пакеты
201
+ // (например, packages/heat/thermostat перед packages/heat).
202
+ //
203
+ // При равной длине — по лексикографическому порядку, чтобы порядок был детерминирован.
204
+ // Это необходимо для корректной идентификации пакета по chunkName.
205
+ //
206
+ packages.sort((a, b) => {
207
+ const lengthDiff = b.workspacePath.length - a.workspacePath.length;
208
+ if (lengthDiff !== 0)
209
+ return lengthDiff;
210
+ return a.workspacePath.localeCompare(b.workspacePath); // Лексикографически
211
+ });
212
+ const frozenPackages = Object.freeze(packages);
213
+ packagesCache.set(rootDir, frozenPackages);
214
+ return frozenPackages;
215
+ }
216
+
217
+ export { resolveMonorepoContextAsync, resolveWorkspaceContextAsync };
package/package.json CHANGED
@@ -1,10 +1,54 @@
1
1
  {
2
2
  "name": "@mirta/workspace",
3
- "version": "0.0.1",
4
- "description": "OIDC trusted publishing setup package for @mirta/workspace",
3
+ "version": "0.4.0",
4
+ "sideEffects": false,
5
+ "description": "Internal resolver for workspace and monorepo context, used by Mirta Framework",
5
6
  "keywords": [
6
- "oidc",
7
- "trusted-publishing",
8
- "setup"
9
- ]
10
- }
7
+ "mirta",
8
+ "workspace",
9
+ "monorepo",
10
+ "context",
11
+ "utility",
12
+ "nodejs",
13
+ "resolver"
14
+ ],
15
+ "license": "Unlicense",
16
+ "homepage": "https://github.com/wb-mirta/core#readme",
17
+ "bugs": {
18
+ "url": "https://github.com/wb-mirta/core/issues"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/wb-mirta/core.git",
23
+ "directory": "packages/mirta-workspace"
24
+ },
25
+ "type": "module",
26
+ "types": "./dist/index.d.mts",
27
+ "files": [
28
+ "dist",
29
+ "LICENSE",
30
+ "README.md"
31
+ ],
32
+ "exports": {
33
+ ".": {
34
+ "import": {
35
+ "types": "./dist/index.d.mts",
36
+ "default": "./dist/index.mjs"
37
+ }
38
+ }
39
+ },
40
+ "imports": {
41
+ "#context/*": "./src/context/*.js",
42
+ "#errors": "./src/errors/index.js"
43
+ },
44
+ "dependencies": {
45
+ "find-up": "^8.0.0",
46
+ "@mirta/package": "0.4.0"
47
+ },
48
+ "engines": {
49
+ "node": ">=24.12.0"
50
+ },
51
+ "scripts": {
52
+ "build:mono": "rollup -c node:@mirta/rollup/config-package"
53
+ }
54
+ }