@mirta/cli 0.3.4 → 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/dist/package.mjs CHANGED
@@ -1,156 +1,68 @@
1
+ import nodePath, { posix } from 'node:path';
2
+ import pMap from 'p-map';
3
+ import { writeFile, glob } from 'node:fs/promises';
4
+ import { resolveMonorepoContextAsync } from '@mirta/workspace';
5
+ import { readPackageAsync, PackageError, resolvePackagePath, toPosix } from '@mirta/package';
6
+ import { T as THIS_PACKAGE_NAME } from './constants.mjs';
7
+ import { l as logger, r as runCommandAsync, t } from './index.mjs';
1
8
  import chalk from 'chalk';
2
- import { a as getLocale, g as getLocalized, u as useLogger, r as runCommandAsync } from './shell.mjs';
3
- import nodePath from 'node:path';
4
- import { readdirSync, statSync, readFileSync, existsSync, writeFileSync } from 'node:fs';
5
-
6
- const { dim, yellow: yellow$1 } = chalk;
7
- const locale = getLocale();
8
- const helpMessageEn = `\
9
- Performs operations over monorepo projects powered by the Mirta Framework.
10
-
11
- ${yellow$1('Usage:')}
12
- mirta [command] [options...]
13
-
14
- ${yellow$1('Commands:')}
15
- - release:
16
- ${dim('Increase package versions following semantic versioning rules.')}
17
- - publish:
18
- ${dim('Builds and publishes packages to npm registry.')}
19
-
20
- ${yellow$1(`Options for 'release':`)}
21
- --dry
22
- ${dim('Runs the command in dry run mode, showing what would be done but not performing any actual changes. Useful for previewing changes before applying them.')}
23
- --preid <custom-pre-release-id>
24
- ${dim('Sets a custom pre-release identifier that will be appended to the version string (for example, beta.1). This option allows creating pre-release versions like alpha, beta, rc etc., prior to official stable releases.')}
25
- --skipPrompts
26
- ${dim('Skips user interaction prompts entirely. The command runs non-interactively, automatically proceeding with defaults or configured values where applicable.')}
27
- --skipGit
28
- ${dim('Omits Git-related actions such as committing changes, tagging commits, or pushing updates to remote repositories. This can be useful if you want to manually manage Git operations later.')}
29
-
30
- ${yellow$1(`Options for 'publish':`)}
31
- --dry
32
- ${dim('Runs the command in dry run mode, showing what would be done but not performing any actual changes. Useful for previewing changes before applying them.')}
33
- --skipBuild
34
- ${dim('Excludes running the build process after version bumps. Bypasses execution of tasks defined in the build pipeline, allowing users to control whether they need a rebuild after updating package versions.')}
35
- --skipGit
36
- ${dim('Omits Git-related actions such as committing changes, tagging commits, or pushing updates to remote repositories. This can be useful if you want to manually manage Git operations later.')}
37
-
38
- `;
39
- const helpMessageRu = `\
40
- Выполняет операции над проектами монорепозитория, работающими на базе фреймворка Mirta.
41
-
42
- ${yellow$1('Использование:')}
43
- mirta [command] [options...]
44
-
45
- ${yellow$1('Команды:')}
46
- - release:
47
- ${dim('Повышение версий пакетов согласно правилам семантического версионирования.')}
48
- - publish:
49
- ${dim('Сборка и публикация пакетов в реестр npm.')}
50
-
51
- ${yellow$1(`Опции для 'release':`)}
52
- --dry
53
- ${dim('Запускает команду в режиме симуляции ("dry run"), показывая изменения, которые будут произведены, но фактически ничего не меняя. Полезно для предварительного просмотра изменений перед применением.')}
54
- --preid <custom-pre-release-id>
55
- ${dim('Устанавливает кастомный префикс для предварительной версии, который добавляется к номеру версии пакета (например, beta.1). Эта опция позволяет создавать предварительные версии типа альфа, бета, RC и др. перед официальным стабильным выпуском.')}
56
- --skipPrompts
57
- ${dim('Пропускает интерактивные запросы пользователя. Команда выполняется автоматически, используя значения по умолчанию или заданные настройки.')}
58
- --skipGit
59
- ${dim('Игнорирует действия, связанные с системой контроля версий Git, такие как фиксация изменений, создание меток коммитов или отправка изменений на удалённый репозиторий. Может пригодиться, если вы хотите самостоятельно управлять операциями с Git позже.')}
60
-
61
- ${yellow$1(`Опции для 'publish':`)}
62
- --dry
63
- ${dim('Запускает команду в режиме симуляции ("dry run"), показывая изменения, которые будут произведены, но фактически ничего не меняя. Полезно для предварительного просмотра изменений перед применением.')}
64
- --skipBuild
65
- ${dim('Исключает запуск процесса сборки после обновления версий пакетов. Пропускает выполнение заданий, указанных в конвейере сборки, позволяя вам самим решать, необходима ли повторная компиляция после изменения номеров версий.')}
66
- --skipGit
67
- ${dim('Игнорирует действия, связанные с системой контроля версий Git, такие как фиксация изменений, создание меток коммитов или отправка изменений на удалённый репозиторий. Может пригодиться, если вы хотите самостоятельно управлять операциями с Git позже.')}
68
- `;
69
- const helpMessage = locale === 'ru-RU'
70
- ? helpMessageRu
71
- : helpMessageEn;
72
9
 
73
10
  const { yellow } = chalk;
74
- const messages = await getLocalized();
75
- const logger = useLogger(messages);
76
- const rootDir = process.cwd();
77
- const packagesDir = nodePath.join(rootDir, 'packages');
78
- const getPackageRoot = (packageName) => nodePath.join(packagesDir, packageName);
79
- // Перечисляем пакеты, которые должны выйти в релиз.
80
- const packages = readdirSync(packagesDir)
81
- .filter((pkgName) => {
82
- const pkgRoot = getPackageRoot(pkgName);
83
- if (!statSync(pkgRoot).isDirectory())
84
- return;
85
- const pkg = JSON.parse(readFileSync(nodePath.join(pkgRoot, 'package.json'), 'utf-8'));
86
- return !pkg.private;
87
- });
88
- const templatePackages = {};
89
- /** Имя пользователя или организации, владеющей пакетом. */
90
- let scope;
91
- /** Форматированный scope пакета. */
92
- let scoped;
93
- /** Использовать scope в качестве префикса в названиях пакетов. */
94
- let scopeAsPackagePrefix = false;
95
- const mirtaConfigFilePath = nodePath.join(rootDir, 'mirta.config.json');
96
- const rootPackage = JSON.parse(readFileSync(nodePath.join(rootDir, 'package.json'), 'utf-8'));
97
- if (rootPackage.name) {
98
- const match = /^@([^/]+)\//i.exec(rootPackage.name);
99
- if (match?.[1]) {
100
- scope = match[1];
101
- scoped = `@${match[1]}/`;
102
- }
11
+ const MAX_CONCURRENT_WRITES = 5;
12
+ const MAX_CONCURRENT_REQUESTS = 5;
13
+ const cwd = process.cwd();
14
+ const context = await resolveMonorepoContextAsync(cwd);
15
+ const rootDir = context.rootDir;
16
+ // Список всех пакетов репозитория.
17
+ const packages = {};
18
+ for (const pkg of context.packages) {
19
+ // Необходимость обновления версии определяется
20
+ // наличием атрибута версии.
21
+ //
22
+ // Не зависит от статуса private пакета.
23
+ //
24
+ if (pkg.version)
25
+ packages[pkg.name] = {
26
+ workspacePath: pkg.workspacePath,
27
+ isPrivate: pkg.isPrivate,
28
+ };
103
29
  }
30
+ const rootPackage = await readPackageAsync(rootDir);
104
31
  /** Возвращает текущую версию корневого проекта. */
105
32
  function getCurrentVersion() {
33
+ if (!rootPackage.version)
34
+ throw PackageError.getScoped(THIS_PACKAGE_NAME, 'noVersionField');
106
35
  return rootPackage.version;
107
36
  }
108
37
  function hasScript(name) {
109
38
  return rootPackage.scripts?.[name] !== void 0;
110
39
  }
111
- if (existsSync(mirtaConfigFilePath)) {
112
- const config = JSON.parse(readFileSync(mirtaConfigFilePath, 'utf-8'));
113
- if (config.scope) {
114
- scope = config.scope;
115
- if (scope.startsWith('@'))
116
- scope = scope.slice(1);
117
- if (scope)
118
- scoped = `@${scope}/`;
40
+ async function resolveTemplatePathsAsync(config) {
41
+ const templates = config.project?.templates;
42
+ if (!Array.isArray(templates))
43
+ return [];
44
+ const pathPatterns = [];
45
+ for (const templatePath of templates) {
46
+ const resolvedDir = toPosix(nodePath.resolve(rootDir, templatePath));
47
+ if (!resolvedDir.startsWith(rootDir + posix.sep)) {
48
+ logger.warn(t('package.templateOutsideRoot', { template: templatePath }));
49
+ continue;
50
+ }
51
+ pathPatterns.push(posix.join(templatePath, '**', 'package.json'));
119
52
  }
120
- scopeAsPackagePrefix = config.scopeAsPackagePrefix === true;
121
- if (config.templates && Array.isArray(config.templates)) {
122
- config.templates.forEach((template) => {
123
- const templatesDir = nodePath.resolve(rootDir, template);
124
- // Предохранитель от выхода за пределы рабочей директории.
125
- if (!templatesDir.startsWith(rootDir))
126
- return;
127
- // Обрабатываем только существующие директории.
128
- if (!statSync(templatesDir).isDirectory())
129
- return;
130
- const localTemplatePackages = readdirSync(templatesDir, {
131
- withFileTypes: true,
132
- recursive: true,
133
- })
134
- .reduce((items, nextEntry) => {
135
- if (nextEntry.name === 'package.json')
136
- items.push(nextEntry.parentPath);
137
- return items;
138
- }, []);
139
- templatePackages[templatesDir] = localTemplatePackages;
140
- });
53
+ const realPaths = new Set();
54
+ if (pathPatterns.length === 0)
55
+ return [];
56
+ for await (const pkgPath of glob(pathPatterns, {
57
+ cwd: rootDir,
58
+ exclude: ['node_modules/**', 'dist/**'],
59
+ })) {
60
+ realPaths.add(toPosix(pkgPath));
141
61
  }
62
+ return [...realPaths];
142
63
  }
143
64
  const isWorkspacePackage = (pkgName) => {
144
- if (!pkgName)
145
- return false;
146
- if (packages.includes(pkgName))
147
- return true;
148
- if (!scoped || !pkgName.startsWith(scoped))
149
- return false;
150
- pkgName = pkgName.slice(scoped.length);
151
- if (scopeAsPackagePrefix)
152
- pkgName = `${scope}-${pkgName}`;
153
- return packages.includes(pkgName);
65
+ return Boolean(pkgName && packages[pkgName]);
154
66
  };
155
67
  function updateDependencies(pkg, depType, version) {
156
68
  const deps = pkg[depType];
@@ -163,39 +75,35 @@ function updateDependencies(pkg, depType, version) {
163
75
  logger.step(`- ${dep}`);
164
76
  });
165
77
  }
166
- function updateTemplateDependencies(templateRoot, version) {
167
- logger.step(`Template: ${nodePath.relative(rootDir, templateRoot)}`);
168
- const pkgPath = nodePath.join(templateRoot, 'package.json');
169
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
78
+ async function updateTemplateDependencies(pkgPath, version) {
79
+ logger.step(pkgPath);
80
+ const pkg = await readPackageAsync(pkgPath);
170
81
  updateDependencies(pkg, 'dependencies', version);
171
82
  updateDependencies(pkg, 'devDependencies', version);
172
- writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
83
+ await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
173
84
  }
174
- function updatePackageVersion(pkgRoot, version) {
175
- const pkgPath = nodePath.join(pkgRoot, 'package.json');
176
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
85
+ async function updatePackageVersion(pkgRoot, version) {
86
+ const pkgPath = resolvePackagePath(pkgRoot);
87
+ const pkg = await readPackageAsync(pkgPath);
177
88
  logger.step(pkgRoot === rootDir
178
- ? 'Root package'
179
- : `Package: ${pkg.name ?? nodePath.basename(pkgRoot)}`);
89
+ ? '- <root>'
90
+ : `- ${pkg.name ?? nodePath.basename(pkgRoot)}`);
180
91
  pkg.version = version;
181
- writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
92
+ await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
182
93
  }
183
- function updateVersion(version) {
94
+ async function updateVersion(version, config) {
184
95
  logger.log(`Patching all packages to version ${version}`);
185
- // Update root package.json
186
- updatePackageVersion(rootDir, version);
187
- // Update all packages
188
- packages.forEach((pkgDirName) => {
189
- updatePackageVersion(nodePath.join(packagesDir, pkgDirName), version);
190
- });
191
- const templateKeys = Object.keys(templatePackages);
192
- if (templateKeys.length > 0) {
96
+ // Обновляет корневой пакет.
97
+ await updatePackageVersion(rootDir, version);
98
+ rootPackage.version = version;
99
+ // Обновляет все остальные пакеты репозитория.
100
+ await pMap(Object.values(packages), ({ workspacePath }) => updatePackageVersion(nodePath.join(rootDir, workspacePath), version), { concurrency: MAX_CONCURRENT_WRITES });
101
+ // Повторное сканирование FS на каждое обновление - для безопасности.
102
+ const templatePaths = await resolveTemplatePathsAsync(config);
103
+ // Обновляет пакеты, используемые в шаблонах.
104
+ if (templatePaths.length > 0) {
193
105
  logger.log(`Patching template packages`);
194
- Object.keys(templatePackages).forEach((templatesDir) => {
195
- templatePackages[templatesDir].forEach((templateRoot) => {
196
- updateTemplateDependencies(templateRoot, version);
197
- });
198
- });
106
+ await pMap(templatePaths, path => updateTemplateDependencies(path, version), { concurrency: MAX_CONCURRENT_WRITES });
199
107
  }
200
108
  }
201
109
  /** Выполняет сборку пакетов стандартной командой `pnpm run build`. */
@@ -208,7 +116,31 @@ async function buildPackagesAsync(skipBuild) {
208
116
  logger.log(`${yellow('Skip')} building packages`);
209
117
  }
210
118
  }
211
- async function publishSinglePackageAsync(pkgName, version, flags) {
119
+ /**
120
+ * Проверяет существование пакета в NPM.
121
+ *
122
+ * @since 0.4.0
123
+ *
124
+ * @internal
125
+ *
126
+ **/
127
+ async function checkPackageExistsAsync(pkgName) {
128
+ try {
129
+ await runCommandAsync('npm', ['view', pkgName, 'version'], {
130
+ stdio: 'pipe',
131
+ });
132
+ return true;
133
+ }
134
+ catch (e) {
135
+ if (e instanceof Error) {
136
+ const message = e.message;
137
+ if (message.includes('404'))
138
+ return false;
139
+ }
140
+ throw e;
141
+ }
142
+ }
143
+ async function publishSinglePackageAsync(pkgName, pkgPath, version, flags) {
212
144
  let releaseTag = void 0;
213
145
  if (version.includes('alpha')) {
214
146
  releaseTag = 'alpha';
@@ -219,7 +151,7 @@ async function publishSinglePackageAsync(pkgName, version, flags) {
219
151
  else if (version.includes('rc')) {
220
152
  releaseTag = 'rc';
221
153
  }
222
- logger.step(`Publishing ${pkgName}`);
154
+ logger.step(t('publish.packagePublishing', { name: pkgName }));
223
155
  try {
224
156
  await runCommandAsync('pnpm', [
225
157
  'publish',
@@ -228,14 +160,14 @@ async function publishSinglePackageAsync(pkgName, version, flags) {
228
160
  'public',
229
161
  ...(flags),
230
162
  ], {
231
- cwd: getPackageRoot(pkgName),
163
+ cwd: pkgPath,
232
164
  stdio: 'pipe',
233
165
  });
234
- logger.success(`Published ${pkgName}@${version}`);
166
+ logger.success(t('publish.packagePublished', { name: `${pkgName}@${version}` }));
235
167
  }
236
168
  catch (e) {
237
169
  if (e instanceof Error && /previously published/.exec(e.message)) {
238
- logger.warn(`Skipping already published ${pkgName}`);
170
+ logger.warn(t('publish.skippingPublished', { name: pkgName }));
239
171
  }
240
172
  else {
241
173
  throw e;
@@ -244,7 +176,25 @@ async function publishSinglePackageAsync(pkgName, version, flags) {
244
176
  }
245
177
  /** Выполняет публикацию пакетов монорепозитория в NPM. */
246
178
  async function publishPackagesAsync(version, skipGitChecks, isDryRun) {
247
- logger.log('Publishing packages...');
179
+ logger.log(t('publish.begin'));
180
+ const packagesToPublish = Object.entries(packages)
181
+ .filter(([, pkg]) => !pkg.isPrivate);
182
+ if (!isDryRun) {
183
+ const existenceChecks = await pMap(packagesToPublish, async ([pkgName]) => ({
184
+ name: pkgName,
185
+ isExists: await checkPackageExistsAsync(pkgName),
186
+ }), { concurrency: MAX_CONCURRENT_REQUESTS, stopOnError: true });
187
+ const unpublishedPackages = existenceChecks
188
+ .filter(({ isExists }) => !isExists)
189
+ .map(({ name }) => name);
190
+ if (unpublishedPackages.length > 0) {
191
+ logger.error(t('publish.newPackages', {
192
+ packages: unpublishedPackages.join(', '),
193
+ }));
194
+ logger.error(t('publish.initialPublishRequired'));
195
+ throw new Error('Packages not found in NPM registry');
196
+ }
197
+ }
248
198
  const flags = [];
249
199
  if (isDryRun)
250
200
  flags.push('--dry-run');
@@ -252,8 +202,14 @@ async function publishPackagesAsync(version, skipGitChecks, isDryRun) {
252
202
  flags.push('--no-git-checks');
253
203
  if (process.env.CI)
254
204
  flags.push('--provenance');
255
- for (const pkgName of packages)
256
- await publishSinglePackageAsync(pkgName, version, flags);
205
+ for (const [pkgName, pkg] of packagesToPublish) {
206
+ // Приватные пакеты не публикуются.
207
+ if (pkg.isPrivate) {
208
+ logger.step(t('publish.skippingPrivate', { name: pkgName }));
209
+ continue;
210
+ }
211
+ await publishSinglePackageAsync(pkgName, posix.join(rootDir, pkg.workspacePath), version, flags);
212
+ }
257
213
  }
258
214
 
259
- export { hasScript as a, buildPackagesAsync as b, getCurrentVersion as g, helpMessage as h, publishPackagesAsync as p, updateVersion as u };
215
+ export { buildPackagesAsync as b, getCurrentVersion as g, hasScript as h, publishPackagesAsync as p, updateVersion as u };
package/dist/publish.mjs CHANGED
@@ -1,54 +1,74 @@
1
- import { parseArgs } from 'node:util';
2
- import { g as getCurrentVersion, h as helpMessage, b as buildPackagesAsync, p as publishPackagesAsync } from './package.mjs';
3
- import cliPackage from '../package.json' with { type: 'json' };
1
+ import { g as getCurrentVersion, b as buildPackagesAsync, p as publishPackagesAsync } from './package.mjs';
2
+ import { a as assertNoParseErrors, l as logger } from './index.mjs';
3
+ import 'node:path';
4
+ import 'p-map';
5
+ import 'node:fs/promises';
6
+ import '@mirta/workspace';
7
+ import '@mirta/package';
8
+ import './constants.mjs';
4
9
  import 'chalk';
5
- import './shell.mjs';
10
+ import 'prompts';
11
+ import '@mirta/staged-args';
6
12
  import 'node:child_process';
7
- import 'node:url';
8
- import 'node:fs';
9
- import 'node:path';
10
- import 'lodash.merge';
13
+ import '@mirta/i18n';
14
+ import '../package.json' with { type: 'json' };
15
+ import '@mirta/basics/fuzzy';
11
16
 
12
- const currentVersion = getCurrentVersion();
13
- const allOptions = ({
14
- dry: {
17
+ const options = ({
18
+ 'dry-run': {
15
19
  type: 'boolean',
16
- default: false,
17
20
  },
18
- skipGit: {
21
+ 'skip-git': {
19
22
  type: 'boolean',
20
- default: false,
21
23
  },
22
- skipBuild: {
24
+ 'skip-build': {
23
25
  type: 'boolean',
24
- default: false,
25
26
  },
26
- help: {
27
+ // Deprecated. Use 'dry-run' instead
28
+ 'dry': {
27
29
  type: 'boolean',
28
- short: 'h',
29
- default: false,
30
30
  },
31
- version: {
31
+ // Deprecated. Use 'skip-git' instead
32
+ 'skipGit': {
33
+ type: 'boolean',
34
+ },
35
+ // Deprecated. Use 'skip-build' instead
36
+ 'skipBuild': {
32
37
  type: 'boolean',
33
- short: 'v',
34
- default: false,
35
38
  },
36
39
  });
37
- const args = process.argv.slice(2);
38
- const { values: argv } = parseArgs({
39
- args,
40
- options: allOptions,
41
- allowPositionals: true,
42
- });
43
- if (argv.help) {
44
- console.log(helpMessage);
45
- process.exit(0);
40
+ function parseArgs(args) {
41
+ const parseResult = args.parseFinal(options);
42
+ assertNoParseErrors(parseResult);
43
+ const { values, positionals } = parseResult.data;
44
+ if (values.dry) {
45
+ logger.warn('Deprecated flag "--dry" used. Please use "--dry-run" instead');
46
+ values['dry-run'] = values['dry-run'] !== false;
47
+ }
48
+ if (values.skipGit) {
49
+ logger.warn('Deprecated flag "--skipGit" used. Please use "--skip-git" instead');
50
+ values['skip-git'] = values['skip-git'] !== false;
51
+ }
52
+ if (values.skipBuild) {
53
+ logger.warn('Deprecated flag "--skipBuild" used. Please use "--skip-build" instead');
54
+ values['skip-build'] = values['skip-build'] !== false;
55
+ }
56
+ return {
57
+ values,
58
+ positionals,
59
+ };
46
60
  }
47
- if (argv.version) {
48
- console.log(`${cliPackage.name} v${cliPackage.version}`);
49
- process.exit(0);
61
+
62
+ async function runAsync(args) {
63
+ // === 1. Парсинг аргументов ===
64
+ const { values: argv } = parseArgs(args);
65
+ const isDryRun = argv['dry-run'] ?? false;
66
+ const skipGit = argv['skip-git'] ?? false;
67
+ const skipBuild = argv['skip-build'] ?? false;
68
+ // === 2. Выполнение сборки и публикации ===
69
+ const currentVersion = getCurrentVersion();
70
+ await buildPackagesAsync(skipBuild);
71
+ await publishPackagesAsync(currentVersion, skipGit, isDryRun);
50
72
  }
51
- const skipGit = argv.skipGit;
52
- const isDryRun = argv.dry;
53
- await buildPackagesAsync(argv.skipBuild);
54
- await publishPackagesAsync(currentVersion, skipGit, isDryRun);
73
+
74
+ export { runAsync };