@retailcrm/embed-ui 0.9.11 → 0.9.13
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/AGENTS.md +2 -0
- package/CHANGELOG.md +22 -0
- package/Makefile +27 -0
- package/README.md +12 -1
- package/bin/embed-ui-update.mjs +421 -33
- package/coverage/clover.xml +914 -568
- package/coverage/coverage-final.json +38 -25
- package/coverage/index.html +89 -59
- package/dist/index.cjs +9 -0
- package/dist/index.mjs +9 -0
- package/package.json +6 -6
package/AGENTS.md
CHANGED
|
@@ -105,4 +105,6 @@ yarn workspace @retailcrm/embed-ui-v1-components run storybook:build
|
|
|
105
105
|
|
|
106
106
|
## Notes
|
|
107
107
|
- Do not assume legacy rules from other repositories (especially `omnica`) apply here.
|
|
108
|
+
- Keep imports grouped by type-only, external, internal alias, and relative blocks, separated from each other and alphabetized within each block: this improves scanability and reduces merge conflicts when multiple PRs add imports to the same file.
|
|
109
|
+
- When resolving lint issues, prefer running `eslint` with `--fix` first to avoid manual import reshuffling and unnecessary reading of repository-specific lint rules.
|
|
108
110
|
- If repository policy is unclear, ask a short clarifying question before making irreversible actions.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## [0.9.13](https://github.com/retailcrm/embed-ui/compare/v0.9.12...v0.9.13) (2026-03-16)
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* Host query contract was added ([b2bfc48](https://github.com/retailcrm/embed-ui/commit/b2bfc486f667bb97efd8d47662e32dfd39d93119))
|
|
9
|
+
* **v1-components:** UiTable stories and tooling were added ([18c89ba](https://github.com/retailcrm/embed-ui/commit/18c89bac8c6481c889e0e0071cd140a6b74520c9))
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **v1-components:** UiTag attrs forwarding was corrected ([e3f653b](https://github.com/retailcrm/embed-ui/commit/e3f653bac555121d06d03f1bc2832296973c8aa9))
|
|
14
|
+
## [0.9.12](https://github.com/retailcrm/embed-ui/compare/v0.9.11...v0.9.12) (2026-03-11)
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* Recursive updater scan and add mode were added ([72725e9](https://github.com/retailcrm/embed-ui/commit/72725e989fbfd8f5192ff226401ee7ad5501ad3e))
|
|
19
|
+
* **v1-components:** Remote component method delegates were typed ([4bfcd4c](https://github.com/retailcrm/embed-ui/commit/4bfcd4c7964f342f581701390a370e504ef213e4))
|
|
20
|
+
* **v1-components:** Vue remote tooling was applied to UiSelect ([2be366c](https://github.com/retailcrm/embed-ui/commit/2be366cec6a9dba32ff0ddb24b0716d6498880a0))
|
|
21
|
+
|
|
22
|
+
### Bug Fixes
|
|
23
|
+
|
|
24
|
+
* **v1-components:** Test lint violations were corrected ([8c0968c](https://github.com/retailcrm/embed-ui/commit/8c0968ccfaaacd37538bb76e2382f253ba20287e))
|
|
25
|
+
* **v1-components:** UiSelect remote tests were aligned with remote tooling ([5c76150](https://github.com/retailcrm/embed-ui/commit/5c7615085c01e6c198527725c54f6f52d4764379))
|
|
4
26
|
## [0.9.11](https://github.com/retailcrm/embed-ui/compare/v0.9.11-alpha.7...v0.9.11) (2026-03-11)
|
|
5
27
|
## [0.9.11-alpha.7](https://github.com/retailcrm/embed-ui/compare/v0.9.11-alpha.6...v0.9.11-alpha.7) (2026-03-06)
|
|
6
28
|
|
package/Makefile
CHANGED
|
@@ -30,6 +30,33 @@ build: .require-compose ## [Build][docker][heavy] Builds all workspaces
|
|
|
30
30
|
$(YARN) workspaces foreach -A --topological-dev run build
|
|
31
31
|
$(TARGET_OK)
|
|
32
32
|
|
|
33
|
+
.PHONY: storybook.build
|
|
34
|
+
storybook.build: .require-compose ## [Build][docker][heavy] Builds Storybook for v1-components
|
|
35
|
+
$(TARGET_HEADER)
|
|
36
|
+
$(YARN) workspace @retailcrm/embed-ui-v1-components run storybook:build
|
|
37
|
+
$(TARGET_OK)
|
|
38
|
+
|
|
39
|
+
.PHONY: storybook.serve
|
|
40
|
+
storybook.serve: .require-compose ## [Build][docker] Runs Storybook for v1-components
|
|
41
|
+
$(TARGET_HEADER)
|
|
42
|
+
$(COMPOSE) up v1-components
|
|
43
|
+
|
|
44
|
+
.PHONY: storybook.shot
|
|
45
|
+
storybook.shot: .require-compose ## [Research][docker] Captures a Storybook screenshot for v1-components docs/story page
|
|
46
|
+
$(TARGET_HEADER)
|
|
47
|
+
@$(COMPOSE) up -d v1-components
|
|
48
|
+
@UID=$$(id -u) GID=$$(id -g) $(COMPOSE) run --rm playwright \
|
|
49
|
+
yarn workspace @retailcrm/embed-ui-v1-components run storybook:shot \
|
|
50
|
+
--base-url http://v1-components:6006 \
|
|
51
|
+
--path "$(if $(story_path),$(story_path),/iframe.html?viewMode=docs&id=components-uitable--docs)" \
|
|
52
|
+
--output "$(if $(output),$(output),artifacts/storybook/UiTable.docs.png)" \
|
|
53
|
+
--wait-for-selector "$(if $(wait_for),$(wait_for),#storybook-docs)" \
|
|
54
|
+
--settle-ms "$(if $(settle_ms),$(settle_ms),2500)" \
|
|
55
|
+
--timeout-ms "$(if $(timeout_ms),$(timeout_ms),60000)" \
|
|
56
|
+
--viewport-width "$(if $(viewport_width),$(viewport_width),1600)" \
|
|
57
|
+
--viewport-height "$(if $(viewport_height),$(viewport_height),1600)"
|
|
58
|
+
$(TARGET_OK)
|
|
59
|
+
|
|
33
60
|
.PHONY: prepare
|
|
34
61
|
prepare: .require-compose ## [Build][docker][heavy] Runs prepare in all workspaces
|
|
35
62
|
$(TARGET_HEADER)
|
package/README.md
CHANGED
|
@@ -8,7 +8,8 @@ API и компоненты для создания расширений инт
|
|
|
8
8
|
|
|
9
9
|
## Обновление версий в целевом проекте
|
|
10
10
|
|
|
11
|
-
Можно запустить бинарник через `npx`, чтобы обновить пакеты `@retailcrm/embed-ui*`
|
|
11
|
+
Можно запустить бинарник через `npx`, чтобы обновить пакеты `@retailcrm/embed-ui*` во всех `package.json`
|
|
12
|
+
текущего рабочего дерева или выбранного поддерева
|
|
12
13
|
(`dependencies`, `devDependencies`, `peerDependencies`, `optionalDependencies`).
|
|
13
14
|
|
|
14
15
|
```bash
|
|
@@ -19,6 +20,16 @@ npx @retailcrm/embed-ui --target ./my-project --dry-run
|
|
|
19
20
|
```
|
|
20
21
|
|
|
21
22
|
По умолчанию используется последняя версия из npm. Флаг `--exact` переключает формат обновления на точную версию.
|
|
23
|
+
CLI сохраняет исходные отступы и переводы строк в каждом изменяемом `package.json`.
|
|
24
|
+
|
|
25
|
+
Для точечного добавления пакетов только в один `package.json` есть флаг `--add`.
|
|
26
|
+
Если не передать `--packages`, CLI откроет интерактивный режим с выбором пакетов и кратким описанием.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx @retailcrm/embed-ui --add
|
|
30
|
+
npx @retailcrm/embed-ui --add --packages components,contexts
|
|
31
|
+
npx @retailcrm/embed-ui --add --target ./my-project --version 0.9.11
|
|
32
|
+
```
|
|
22
33
|
|
|
23
34
|
## Цели встраивания
|
|
24
35
|
|
package/bin/embed-ui-update.mjs
CHANGED
|
@@ -1,26 +1,82 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { createInterface } from 'node:readline/promises'
|
|
3
4
|
import { execFileSync } from 'node:child_process'
|
|
4
5
|
import fs from 'node:fs'
|
|
5
6
|
import path from 'node:path'
|
|
7
|
+
import { pathToFileURL } from 'node:url'
|
|
6
8
|
import process from 'node:process'
|
|
7
9
|
|
|
8
|
-
const ROOT_PACKAGE = '@retailcrm/embed-ui'
|
|
9
|
-
const TARGET_SECTIONS = [
|
|
10
|
+
export const ROOT_PACKAGE = '@retailcrm/embed-ui'
|
|
11
|
+
export const TARGET_SECTIONS = [
|
|
10
12
|
'dependencies',
|
|
11
13
|
'devDependencies',
|
|
12
14
|
'peerDependencies',
|
|
13
15
|
'optionalDependencies',
|
|
14
16
|
]
|
|
15
17
|
|
|
18
|
+
export const INSTALLABLE_PACKAGES = [
|
|
19
|
+
{
|
|
20
|
+
id: 'embed-ui',
|
|
21
|
+
name: ROOT_PACKAGE,
|
|
22
|
+
section: 'dependencies',
|
|
23
|
+
description: 'Базовый пакет с общим API и согласованными v1-зависимостями.',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'components',
|
|
27
|
+
name: '@retailcrm/embed-ui-v1-components',
|
|
28
|
+
section: 'dependencies',
|
|
29
|
+
description: 'UI-компоненты для host/remote приложений.',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'contexts',
|
|
33
|
+
name: '@retailcrm/embed-ui-v1-contexts',
|
|
34
|
+
section: 'dependencies',
|
|
35
|
+
description: 'Реактивные контексты RetailCRM JS API.',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'types',
|
|
39
|
+
name: '@retailcrm/embed-ui-v1-types',
|
|
40
|
+
section: 'dependencies',
|
|
41
|
+
description: 'Базовые type declarations для RetailCRM JS API.',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'testing',
|
|
45
|
+
name: '@retailcrm/embed-ui-v1-testing',
|
|
46
|
+
section: 'devDependencies',
|
|
47
|
+
description: 'Вспомогательные утилиты и типы для тестов интеграций.',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'endpoint',
|
|
51
|
+
name: '@retailcrm/embed-ui-v1-endpoint',
|
|
52
|
+
section: 'dependencies',
|
|
53
|
+
description: 'Endpoint API для интеграций в RetailCRM.',
|
|
54
|
+
},
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
const DEFAULT_INDENT = ' '
|
|
58
|
+
const DEFAULT_NEWLINE = '\n'
|
|
59
|
+
const SKIP_DIRECTORIES = new Set([
|
|
60
|
+
'.git',
|
|
61
|
+
'.hg',
|
|
62
|
+
'.svn',
|
|
63
|
+
'.yarn',
|
|
64
|
+
'node_modules',
|
|
65
|
+
'dist',
|
|
66
|
+
'build',
|
|
67
|
+
'coverage',
|
|
68
|
+
])
|
|
69
|
+
|
|
16
70
|
const HELP_TEXT = `Usage:
|
|
17
71
|
npx @retailcrm/embed-ui [target] [version] [options]
|
|
18
72
|
|
|
19
73
|
Options:
|
|
20
|
-
-t, --target <path> Target
|
|
74
|
+
-t, --target <path> Target path (default: current directory)
|
|
21
75
|
-v, --version <ver> Target version. If omitted, latest npm version is used
|
|
22
76
|
--exact Use exact version instead of range
|
|
23
77
|
--dry-run Show changes without writing package.json
|
|
78
|
+
--add Add selected embed-ui packages into one package.json
|
|
79
|
+
--packages <list> Comma-separated package ids or names for --add
|
|
24
80
|
-h, --help Show this help
|
|
25
81
|
|
|
26
82
|
Examples:
|
|
@@ -28,17 +84,27 @@ Examples:
|
|
|
28
84
|
npx @retailcrm/embed-ui --version 0.9.11
|
|
29
85
|
npx @retailcrm/embed-ui ./my-project 0.9.11
|
|
30
86
|
npx @retailcrm/embed-ui --target ./my-project --dry-run
|
|
87
|
+
npx @retailcrm/embed-ui --add
|
|
88
|
+
npx @retailcrm/embed-ui --add --packages components,contexts
|
|
31
89
|
`
|
|
32
90
|
|
|
33
91
|
const isSemverLike = (value) => /^v?\d+\.\d+\.\d+/.test(value)
|
|
34
92
|
const stripLeadingV = (value) => value.replace(/^v/, '')
|
|
35
93
|
|
|
36
|
-
const
|
|
94
|
+
const parsePackageList = (value) =>
|
|
95
|
+
value
|
|
96
|
+
.split(',')
|
|
97
|
+
.map((entry) => entry.trim())
|
|
98
|
+
.filter(Boolean)
|
|
99
|
+
|
|
100
|
+
export const parseArgs = (argv) => {
|
|
37
101
|
const options = {
|
|
38
102
|
target: process.cwd(),
|
|
39
103
|
version: null,
|
|
40
104
|
dryRun: false,
|
|
41
105
|
exact: false,
|
|
106
|
+
add: false,
|
|
107
|
+
packages: null,
|
|
42
108
|
}
|
|
43
109
|
|
|
44
110
|
const positionals = []
|
|
@@ -73,6 +139,17 @@ const parseArgs = (argv) => {
|
|
|
73
139
|
continue
|
|
74
140
|
}
|
|
75
141
|
|
|
142
|
+
if (argument === '--packages') {
|
|
143
|
+
const value = argv[index + 1]
|
|
144
|
+
if (!value) {
|
|
145
|
+
throw new Error('Option --packages requires a value')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
options.packages = parsePackageList(value)
|
|
149
|
+
index++
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
|
|
76
153
|
if (argument === '--dry-run') {
|
|
77
154
|
options.dryRun = true
|
|
78
155
|
continue
|
|
@@ -83,6 +160,11 @@ const parseArgs = (argv) => {
|
|
|
83
160
|
continue
|
|
84
161
|
}
|
|
85
162
|
|
|
163
|
+
if (argument === '--add') {
|
|
164
|
+
options.add = true
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
|
|
86
168
|
if (argument.startsWith('-')) {
|
|
87
169
|
throw new Error(`Unknown option: ${argument}`)
|
|
88
170
|
}
|
|
@@ -111,10 +193,14 @@ const parseArgs = (argv) => {
|
|
|
111
193
|
options.version = stripLeadingV(positionals[1])
|
|
112
194
|
}
|
|
113
195
|
|
|
196
|
+
if (options.packages && !options.add) {
|
|
197
|
+
throw new Error('Option --packages can only be used together with --add')
|
|
198
|
+
}
|
|
199
|
+
|
|
114
200
|
return options
|
|
115
201
|
}
|
|
116
202
|
|
|
117
|
-
const resolveLatestVersion = () => {
|
|
203
|
+
export const resolveLatestVersion = () => {
|
|
118
204
|
const output = execFileSync(
|
|
119
205
|
'npm',
|
|
120
206
|
['view', ROOT_PACKAGE, 'version'],
|
|
@@ -131,10 +217,12 @@ const resolveLatestVersion = () => {
|
|
|
131
217
|
return output
|
|
132
218
|
}
|
|
133
219
|
|
|
134
|
-
const isTargetPackage = (name) =>
|
|
220
|
+
export const isTargetPackage = (name) =>
|
|
135
221
|
name === ROOT_PACKAGE || name.startsWith(`${ROOT_PACKAGE}-`)
|
|
136
222
|
|
|
137
|
-
const
|
|
223
|
+
const createRange = (version, exact) => exact ? version : `^${version}`
|
|
224
|
+
|
|
225
|
+
export const formatRange = (currentRange, nextVersion, exact) => {
|
|
138
226
|
if (exact) {
|
|
139
227
|
return nextVersion
|
|
140
228
|
}
|
|
@@ -154,18 +242,107 @@ const formatRange = (currentRange, nextVersion, exact) => {
|
|
|
154
242
|
return `^${nextVersion}`
|
|
155
243
|
}
|
|
156
244
|
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
const
|
|
245
|
+
export const detectFormatting = (source) => {
|
|
246
|
+
const newline = source.includes('\r\n') ? '\r\n' : DEFAULT_NEWLINE
|
|
247
|
+
const indentMatch = source.match(/\n([ \t]+)"/)
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
indent: indentMatch?.[1] ?? DEFAULT_INDENT,
|
|
251
|
+
newline,
|
|
252
|
+
trailingNewline: source.endsWith('\n') || source.endsWith('\r\n'),
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export const serializePackageJson = (packageJson, formatting) => {
|
|
257
|
+
const serialized = JSON.stringify(packageJson, null, formatting.indent)
|
|
258
|
+
.replace(/\n/g, formatting.newline)
|
|
259
|
+
|
|
260
|
+
return formatting.trailingNewline
|
|
261
|
+
? `${serialized}${formatting.newline}`
|
|
262
|
+
: serialized
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const ensureDirectoryExists = (targetPath) => {
|
|
266
|
+
if (!fs.existsSync(targetPath)) {
|
|
267
|
+
throw new Error(`Path not found: ${targetPath}`)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const stat = fs.statSync(targetPath)
|
|
271
|
+
if (!stat.isDirectory()) {
|
|
272
|
+
throw new Error(`Target is not a directory: ${targetPath}`)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export const resolvePackageJsonPath = (targetPath) => {
|
|
277
|
+
if (path.basename(targetPath) === 'package.json') {
|
|
278
|
+
if (!fs.existsSync(targetPath)) {
|
|
279
|
+
throw new Error(`package.json not found: ${targetPath}`)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return targetPath
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const packageJsonPath = path.resolve(targetPath, 'package.json')
|
|
160
286
|
|
|
161
|
-
const packageJsonPath = path.resolve(options.target, 'package.json')
|
|
162
287
|
if (!fs.existsSync(packageJsonPath)) {
|
|
163
288
|
throw new Error(`package.json not found: ${packageJsonPath}`)
|
|
164
289
|
}
|
|
165
290
|
|
|
166
|
-
|
|
167
|
-
|
|
291
|
+
return packageJsonPath
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export const collectPackageJsonPaths = (targetPath) => {
|
|
295
|
+
const resolvedTarget = path.resolve(targetPath)
|
|
296
|
+
|
|
297
|
+
if (!fs.existsSync(resolvedTarget)) {
|
|
298
|
+
throw new Error(`Path not found: ${resolvedTarget}`)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (path.basename(resolvedTarget) === 'package.json') {
|
|
302
|
+
return [resolvedTarget]
|
|
303
|
+
}
|
|
168
304
|
|
|
305
|
+
ensureDirectoryExists(resolvedTarget)
|
|
306
|
+
|
|
307
|
+
const packageJsonPaths = []
|
|
308
|
+
|
|
309
|
+
const visit = (directoryPath) => {
|
|
310
|
+
const packageJsonPath = path.join(directoryPath, 'package.json')
|
|
311
|
+
if (fs.existsSync(packageJsonPath) && fs.statSync(packageJsonPath).isFile()) {
|
|
312
|
+
packageJsonPaths.push(packageJsonPath)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
for (const entry of fs.readdirSync(directoryPath, { withFileTypes: true })) {
|
|
316
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) {
|
|
317
|
+
continue
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (SKIP_DIRECTORIES.has(entry.name)) {
|
|
321
|
+
continue
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
visit(path.join(directoryPath, entry.name))
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
visit(resolvedTarget)
|
|
329
|
+
|
|
330
|
+
return packageJsonPaths.sort()
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const findDependencySection = (packageJson, packageName) => {
|
|
334
|
+
for (const section of TARGET_SECTIONS) {
|
|
335
|
+
const dependencyMap = packageJson[section]
|
|
336
|
+
|
|
337
|
+
if (dependencyMap && typeof dependencyMap === 'object' && packageName in dependencyMap) {
|
|
338
|
+
return section
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return null
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export const updatePackageJson = (packageJson, version, exact) => {
|
|
169
346
|
const updates = []
|
|
170
347
|
|
|
171
348
|
for (const section of TARGET_SECTIONS) {
|
|
@@ -176,21 +353,18 @@ const main = () => {
|
|
|
176
353
|
}
|
|
177
354
|
|
|
178
355
|
for (const [name, currentRange] of Object.entries(dependencyMap)) {
|
|
179
|
-
if (!isTargetPackage(name)) {
|
|
180
|
-
continue
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (typeof currentRange !== 'string') {
|
|
356
|
+
if (!isTargetPackage(name) || typeof currentRange !== 'string') {
|
|
184
357
|
continue
|
|
185
358
|
}
|
|
186
359
|
|
|
187
|
-
const nextRange = formatRange(currentRange, version,
|
|
360
|
+
const nextRange = formatRange(currentRange, version, exact)
|
|
188
361
|
if (nextRange === currentRange) {
|
|
189
362
|
continue
|
|
190
363
|
}
|
|
191
364
|
|
|
192
365
|
dependencyMap[name] = nextRange
|
|
193
366
|
updates.push({
|
|
367
|
+
type: 'update',
|
|
194
368
|
section,
|
|
195
369
|
name,
|
|
196
370
|
currentRange,
|
|
@@ -199,31 +373,245 @@ const main = () => {
|
|
|
199
373
|
}
|
|
200
374
|
}
|
|
201
375
|
|
|
202
|
-
|
|
203
|
-
|
|
376
|
+
return updates
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export const resolveInstallPackages = (tokens) => {
|
|
380
|
+
const selectedPackages = []
|
|
381
|
+
const seen = new Set()
|
|
382
|
+
|
|
383
|
+
for (const token of tokens) {
|
|
384
|
+
const normalized = token.trim()
|
|
385
|
+
if (!normalized) {
|
|
386
|
+
continue
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const numericIndex = Number(normalized)
|
|
390
|
+
const selectedPackage =
|
|
391
|
+
Number.isInteger(numericIndex) && numericIndex >= 1 && numericIndex <= INSTALLABLE_PACKAGES.length
|
|
392
|
+
? INSTALLABLE_PACKAGES[numericIndex - 1]
|
|
393
|
+
: INSTALLABLE_PACKAGES.find((entry) => entry.id === normalized || entry.name === normalized)
|
|
394
|
+
|
|
395
|
+
if (!selectedPackage) {
|
|
396
|
+
const supported = INSTALLABLE_PACKAGES
|
|
397
|
+
.map((entry, index) => `${index + 1}/${entry.id}/${entry.name}`)
|
|
398
|
+
.join(', ')
|
|
399
|
+
|
|
400
|
+
throw new Error(`Unknown add target "${normalized}". Supported values: ${supported}`)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (seen.has(selectedPackage.name)) {
|
|
404
|
+
continue
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
seen.add(selectedPackage.name)
|
|
408
|
+
selectedPackages.push(selectedPackage)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return selectedPackages
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export const installPackages = (packageJson, packages, version, exact) => {
|
|
415
|
+
const updates = []
|
|
416
|
+
|
|
417
|
+
for (const selectedPackage of packages) {
|
|
418
|
+
const section = findDependencySection(packageJson, selectedPackage.name) ?? selectedPackage.section
|
|
419
|
+
const dependencyMap = packageJson[section] ?? {}
|
|
420
|
+
|
|
421
|
+
if (!(section in packageJson)) {
|
|
422
|
+
packageJson[section] = dependencyMap
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const currentRange = dependencyMap[selectedPackage.name]
|
|
426
|
+
const nextRange = typeof currentRange === 'string'
|
|
427
|
+
? formatRange(currentRange, version, exact)
|
|
428
|
+
: createRange(version, exact)
|
|
429
|
+
|
|
430
|
+
if (currentRange === nextRange) {
|
|
431
|
+
continue
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
dependencyMap[selectedPackage.name] = nextRange
|
|
435
|
+
updates.push({
|
|
436
|
+
type: typeof currentRange === 'string' ? 'update' : 'install',
|
|
437
|
+
section,
|
|
438
|
+
name: selectedPackage.name,
|
|
439
|
+
currentRange: typeof currentRange === 'string' ? currentRange : null,
|
|
440
|
+
nextRange,
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return updates
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export const promptForInstallSelection = async (packageJson) => {
|
|
448
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
449
|
+
throw new Error('Interactive add mode requires a TTY. Use --packages to select packages explicitly.')
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
console.log('Выберите пакеты для установки в текущий package.json:')
|
|
453
|
+
for (const [index, selectedPackage] of INSTALLABLE_PACKAGES.entries()) {
|
|
454
|
+
const currentSection = findDependencySection(packageJson, selectedPackage.name)
|
|
455
|
+
const installedHint = currentSection ? ` Уже есть в ${currentSection}.` : ''
|
|
456
|
+
|
|
457
|
+
console.log(` ${index + 1}. ${selectedPackage.name} (${selectedPackage.id})`)
|
|
458
|
+
console.log(` ${selectedPackage.description} Раздел по умолчанию: ${selectedPackage.section}.${installedHint}`)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const readline = createInterface({
|
|
462
|
+
input: process.stdin,
|
|
463
|
+
output: process.stdout,
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
while (true) {
|
|
468
|
+
const answer = await readline.question(
|
|
469
|
+
'Введите номера, ids или имена пакетов через запятую (например: 1,3 или components,types): '
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
const tokens = parsePackageList(answer)
|
|
473
|
+
if (tokens.length === 0) {
|
|
474
|
+
return []
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
try {
|
|
478
|
+
return resolveInstallPackages(tokens)
|
|
479
|
+
} catch (error) {
|
|
480
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
481
|
+
console.error(message)
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
} finally {
|
|
485
|
+
readline.close()
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const readPackageJson = (packageJsonPath) => {
|
|
490
|
+
const source = fs.readFileSync(packageJsonPath, 'utf8')
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
formatting: detectFormatting(source),
|
|
494
|
+
packageJson: JSON.parse(source),
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const writePackageJson = (packageJsonPath, packageJson, formatting) => {
|
|
499
|
+
fs.writeFileSync(packageJsonPath, serializePackageJson(packageJson, formatting), 'utf8')
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const printChanges = (changes) => {
|
|
503
|
+
for (const change of changes) {
|
|
504
|
+
const prefix = change.type === 'install'
|
|
505
|
+
? `${change.section}: ${change.name} -> ${change.nextRange}`
|
|
506
|
+
: `${change.section}: ${change.name} ${change.currentRange} -> ${change.nextRange}`
|
|
507
|
+
|
|
508
|
+
console.log(` ${prefix}`)
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export const runUpdate = (options) => {
|
|
513
|
+
const version = options.version ?? resolveLatestVersion()
|
|
514
|
+
const packageJsonPaths = collectPackageJsonPaths(options.target)
|
|
515
|
+
const reports = []
|
|
516
|
+
|
|
517
|
+
for (const packageJsonPath of packageJsonPaths) {
|
|
518
|
+
const { formatting, packageJson } = readPackageJson(packageJsonPath)
|
|
519
|
+
const updates = updatePackageJson(packageJson, version, options.exact)
|
|
520
|
+
|
|
521
|
+
if (updates.length === 0) {
|
|
522
|
+
continue
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (!options.dryRun) {
|
|
526
|
+
writePackageJson(packageJsonPath, packageJson, formatting)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
reports.push({ packageJsonPath, updates })
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (reports.length === 0) {
|
|
533
|
+
console.log(`No ${ROOT_PACKAGE}* dependencies found or changed under ${path.resolve(options.target)}`)
|
|
204
534
|
return
|
|
205
535
|
}
|
|
206
536
|
|
|
537
|
+
const totalUpdates = reports.reduce((sum, report) => sum + report.updates.length, 0)
|
|
538
|
+
|
|
207
539
|
console.log(`Resolved version: ${version}`)
|
|
208
|
-
for (const
|
|
209
|
-
console.log(
|
|
210
|
-
|
|
211
|
-
)
|
|
540
|
+
for (const report of reports) {
|
|
541
|
+
console.log(report.packageJsonPath)
|
|
542
|
+
printChanges(report.updates)
|
|
212
543
|
}
|
|
213
544
|
|
|
545
|
+
if (options.dryRun) {
|
|
546
|
+
console.log('Dry run enabled, package.json files were not modified')
|
|
547
|
+
return
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
console.log(
|
|
551
|
+
`Updated ${totalUpdates} dependency entries in ${reports.length} package.json file(s) under ${path.resolve(options.target)}`
|
|
552
|
+
)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export const runAdd = async (options) => {
|
|
556
|
+
const version = options.version ?? resolveLatestVersion()
|
|
557
|
+
const packageJsonPath = resolvePackageJsonPath(path.resolve(options.target))
|
|
558
|
+
const { formatting, packageJson } = readPackageJson(packageJsonPath)
|
|
559
|
+
const selectedPackages = options.packages
|
|
560
|
+
? resolveInstallPackages(options.packages)
|
|
561
|
+
: await promptForInstallSelection(packageJson)
|
|
562
|
+
|
|
563
|
+
if (selectedPackages.length === 0) {
|
|
564
|
+
console.log('Nothing selected, package.json was not modified')
|
|
565
|
+
return
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const updates = installPackages(packageJson, selectedPackages, version, options.exact)
|
|
569
|
+
|
|
570
|
+
if (updates.length === 0) {
|
|
571
|
+
console.log(`Selected packages are already installed with matching ranges in ${packageJsonPath}`)
|
|
572
|
+
return
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
console.log(`Resolved version: ${version}`)
|
|
576
|
+
console.log(packageJsonPath)
|
|
577
|
+
printChanges(updates)
|
|
578
|
+
|
|
214
579
|
if (options.dryRun) {
|
|
215
580
|
console.log('Dry run enabled, package.json was not modified')
|
|
216
581
|
return
|
|
217
582
|
}
|
|
218
583
|
|
|
219
|
-
|
|
220
|
-
console.log(`
|
|
584
|
+
writePackageJson(packageJsonPath, packageJson, formatting)
|
|
585
|
+
console.log(`Installed ${updates.length} package entries in ${packageJsonPath}`)
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export const main = async (argv = process.argv.slice(2)) => {
|
|
589
|
+
const options = parseArgs(argv)
|
|
590
|
+
|
|
591
|
+
if (options.add) {
|
|
592
|
+
await runAdd(options)
|
|
593
|
+
return
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
runUpdate(options)
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const isExecutedDirectly = () => {
|
|
600
|
+
const entryPath = process.argv[1]
|
|
601
|
+
|
|
602
|
+
if (!entryPath) {
|
|
603
|
+
return false
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return pathToFileURL(path.resolve(entryPath)).href === import.meta.url
|
|
221
607
|
}
|
|
222
608
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
609
|
+
if (isExecutedDirectly()) {
|
|
610
|
+
try {
|
|
611
|
+
await main()
|
|
612
|
+
} catch (error) {
|
|
613
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
614
|
+
console.error(message)
|
|
615
|
+
process.exit(1)
|
|
616
|
+
}
|
|
229
617
|
}
|