@deot/dev-releaser 1.1.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/README.md ADDED
@@ -0,0 +1,6 @@
1
+ # @deot/dev-releaser
2
+
3
+ 用于发包流程, 自动生成changelog, tags....
4
+
5
+ - 默认`dryRun`
6
+ - 使用`dependencies`,非`peerDependencies`。主要考虑仅安装`@deot/dev-releaser`即可
@@ -0,0 +1,559 @@
1
+ 'use strict';
2
+
3
+ var chalk = require('chalk');
4
+ var devShared = require('@deot/dev-shared');
5
+ var path = require('node:path');
6
+ var fs = require('fs-extra');
7
+ var node_module = require('node:module');
8
+ var parser = require('conventional-commits-parser');
9
+ var semver = require('semver');
10
+ var inquirer = require('inquirer');
11
+
12
+ function _interopNamespaceDefault(e) {
13
+ var n = Object.create(null);
14
+ if (e) {
15
+ Object.keys(e).forEach(function (k) {
16
+ if (k !== 'default') {
17
+ var d = Object.getOwnPropertyDescriptor(e, k);
18
+ Object.defineProperty(n, k, d.get ? d : {
19
+ enumerable: true,
20
+ get: function () { return e[k]; }
21
+ });
22
+ }
23
+ });
24
+ }
25
+ n.default = e;
26
+ return Object.freeze(n);
27
+ }
28
+
29
+ var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
30
+
31
+ const cwd = process.cwd();
32
+ const require$ = node_module.createRequire(cwd);
33
+ const { prompt } = inquirer;
34
+ const HASH = '-hash-';
35
+ const SUFFIX = '🐒💨🙊';
36
+ const parserOptions = {
37
+ noteKeywords: ['BREAKING CHANGE', 'Breaking Change']
38
+ };
39
+ const reBreaking = new RegExp(`(${parserOptions.noteKeywords.join(')|(')})`);
40
+ class Release {
41
+ packageDir;
42
+ packageName;
43
+ packageFolderName;
44
+ packageOptions;
45
+ packageRelation;
46
+ config;
47
+ changeLog;
48
+ version;
49
+ commits;
50
+ commandOptions;
51
+ constructor(config, commandOptions) {
52
+ const { packageDir, packageRelation } = devShared.Locals.impl();
53
+ if (typeof config === 'string') {
54
+ let packageFolderName = config;
55
+ let packageDir$ = path__namespace.resolve(packageDir, packageFolderName);
56
+ config = {
57
+ dir: packageDir$,
58
+ name: packageFolderName
59
+ };
60
+ }
61
+ this.packageDir = config.dir;
62
+ this.packageName = devShared.Locals.getPackageName(config.name);
63
+ this.packageFolderName = config.name;
64
+ this.packageOptions = require$(`${this.packageDir}/package.json`);
65
+ this.packageRelation = packageRelation[this.packageName] || [];
66
+ this.config = config;
67
+ this.commits = [];
68
+ this.changeLog = '';
69
+ this.version = '';
70
+ this.commandOptions = commandOptions;
71
+ }
72
+ async parseCommits() {
73
+ const { workspace } = devShared.Locals.impl();
74
+ const { packageFolderName, packageName, commandOptions } = this;
75
+ let params = ['tag', '--list', `'${packageName}@*'`, '--sort', '-v:refname'];
76
+ const { stdout: tags } = await devShared.Shell.exec('git', params);
77
+ const [latestTag] = tags.split('\n');
78
+ devShared.Logger.log(chalk.yellow(`Last Release Tag`) + `: ${latestTag || '<none>'}`);
79
+ params = ['--no-pager', 'log', `${latestTag}..HEAD`, `--format=%B%n${HASH}%n%H${SUFFIX}`];
80
+ let { stdout } = await devShared.Shell.exec('git', params);
81
+ let skipGetLog = false;
82
+ if (latestTag) {
83
+ const log1 = await devShared.Shell.exec('git', ['rev-parse', latestTag]);
84
+ const log2 = await devShared.Shell.exec('git', ['--no-pager', 'log', '-1', '--format=%H']);
85
+ if (log1.stdout === log2.stdout) {
86
+ skipGetLog = true;
87
+ }
88
+ }
89
+ if (!skipGetLog && !stdout) {
90
+ if (latestTag) {
91
+ params.splice(2, 1, `${latestTag}`);
92
+ }
93
+ else {
94
+ params.splice(2, 1, 'HEAD');
95
+ }
96
+ ({ stdout } = await devShared.Shell.exec('git', params));
97
+ }
98
+ const allowTypes = ['feat', `fix`, `break change`, `style`, `perf`, `types`, `refactor`, `chore`];
99
+ const rePlugin = new RegExp(`^(${allowTypes.join('|')})${workspace ? `\\(${packageFolderName}\\)` : '(\\(.+\\))?'}: .*`, 'i');
100
+ const allCommits = stdout.split(SUFFIX);
101
+ const commits = allCommits
102
+ .filter((commit) => {
103
+ const chunk = commit.trim();
104
+ return chunk && rePlugin.test(chunk);
105
+ })
106
+ .map((commit) => {
107
+ const node = parser.sync(commit);
108
+ const body = (node.body || node.footer);
109
+ if (!node.type)
110
+ node.type = parser.sync(node.header?.replace(/\(.+\)!?:/, ':') || '').type;
111
+ if (!node.hash)
112
+ node.hash = commit.split(HASH).pop()?.trim();
113
+ node.breaking = reBreaking.test(body) || /!:/.test(node.header);
114
+ node.effect = false;
115
+ node.custom = false;
116
+ return node;
117
+ });
118
+ if (!commits.length) {
119
+ devShared.Logger.log(chalk.red(`No Commits Found.`));
120
+ }
121
+ else {
122
+ devShared.Logger.log(chalk.yellow(`Found `)
123
+ + chalk.bold(`${allCommits.length}`)
124
+ + ` Commits, `
125
+ + chalk.bold(`${commits.length}`)
126
+ + ' Commits Valid');
127
+ }
128
+ const { skipUpdatePackage } = commandOptions;
129
+ if (commits.length && skipUpdatePackage) {
130
+ let skip = false;
131
+ if (typeof skipUpdatePackage === 'boolean' && skipUpdatePackage) {
132
+ let result = await prompt([
133
+ {
134
+ type: 'confirm',
135
+ name: 'skip',
136
+ message: `Skip Update(${this.packageName}@${this.packageOptions.version}):`,
137
+ default: true
138
+ }
139
+ ]);
140
+ skip = result.skip;
141
+ }
142
+ else if (typeof skipUpdatePackage === 'string'
143
+ && (skipUpdatePackage === '**'
144
+ || skipUpdatePackage.split(',').includes(this.packageName))) {
145
+ skip = true;
146
+ }
147
+ if (skip) {
148
+ devShared.Logger.log(chalk.red(`Skipping Update\n`));
149
+ return;
150
+ }
151
+ }
152
+ await this.updateVersion();
153
+ await this.updateCommits(commits);
154
+ const { forceUpdatePackage } = commandOptions;
155
+ if (!commits.length && forceUpdatePackage) {
156
+ let force = false;
157
+ if (typeof forceUpdatePackage === 'boolean' && forceUpdatePackage) {
158
+ let result = await prompt([
159
+ {
160
+ type: 'confirm',
161
+ name: 'force',
162
+ message: `Force Update(${this.packageName}@${this.packageOptions.version}):`,
163
+ default: true
164
+ }
165
+ ]);
166
+ force = result.force;
167
+ }
168
+ else if (typeof forceUpdatePackage === 'string'
169
+ && (forceUpdatePackage === '**'
170
+ || forceUpdatePackage.split(',').includes(this.packageName))) {
171
+ force = true;
172
+ }
173
+ if (force) {
174
+ const oldVersion = this.packageOptions.version;
175
+ const versionChanged = `\`${oldVersion}\` -> \`${this.version}\``;
176
+ this.commits = [
177
+ {
178
+ type: 'chore',
179
+ header: `chore(${this.packageFolderName || 'release'}): force-publish ${versionChanged}`,
180
+ hash: '',
181
+ effect: false,
182
+ breaking: false,
183
+ custom: true
184
+ }
185
+ ];
186
+ this.changeLog = `### Force Update Package\n\n- ${versionChanged}`.trim();
187
+ }
188
+ }
189
+ }
190
+ rebuildChangeLog(commits) {
191
+ const { packageDir } = this;
192
+ const { homepage, workspace } = devShared.Locals.impl();
193
+ const logPath = path__namespace.resolve(packageDir, './CHANGELOG.md');
194
+ const logFile = fs.existsSync(logPath) ? fs.readFileSync(logPath, 'utf-8') : '';
195
+ const notes = {
196
+ breaking: [],
197
+ features: [],
198
+ fixes: [],
199
+ updates: []
200
+ };
201
+ const closeRegxp = /\(?(closes? )\(?#((\d+))\)/ig;
202
+ const pullRegxp = /(?<!closes? )\((#(\d+))\)/ig;
203
+ for (const commit of commits) {
204
+ const { effect, breaking, hash, header, type } = commit;
205
+ const ref = !hash || pullRegxp.test(header)
206
+ ? ''
207
+ : ` ([${hash?.substring(0, 7)}](${homepage}/commit/${hash}))`;
208
+ let message = header?.trim();
209
+ if (workspace && !effect) {
210
+ message = message.replace(/\(.+\)!?:/, ':');
211
+ }
212
+ message = message
213
+ .replace(pullRegxp, `[$1](${homepage}/pull/$2)`)
214
+ .replace(closeRegxp, `[$1$2](${homepage}/issues/$2)`) + ref;
215
+ if (breaking) {
216
+ notes.breaking.push(message);
217
+ }
218
+ else if (type === 'fix') {
219
+ notes.fixes.push(message);
220
+ }
221
+ else if (type === 'feat') {
222
+ notes.features.push(message);
223
+ }
224
+ else {
225
+ notes.updates.push(message);
226
+ }
227
+ }
228
+ Object.keys(notes).forEach(i => {
229
+ notes[i] = notes[i].filter((j) => {
230
+ return !logFile.includes(j);
231
+ });
232
+ });
233
+ const parts = [
234
+ notes.breaking.length ? `### Breaking Changes\n\n- ${notes.breaking.join('\n- ')}`.trim() : '',
235
+ notes.fixes.length ? `### Bugfixes\n\n- ${notes.fixes.join('\n- ')}`.trim() : '',
236
+ notes.features.length ? `### Features\n\n- ${notes.features.join('\n- ')}`.trim() : '',
237
+ notes.updates.length ? `### Updates\n\n- ${notes.updates.join('\n- ')}`.trim() : ''
238
+ ].filter(Boolean);
239
+ const newLog = parts.join('\n\n');
240
+ return !parts.length || logFile.includes(newLog)
241
+ ? ''
242
+ : newLog;
243
+ }
244
+ async updateVersion() {
245
+ const { packageOptions, commits, commandOptions } = this;
246
+ const { version } = packageOptions;
247
+ let newVersion = '';
248
+ if (commandOptions.customVersion) {
249
+ newVersion = commandOptions.customVersion;
250
+ if (!(/\d+.\d+.\d+/.test(newVersion)) || version === newVersion) {
251
+ let result = await prompt([
252
+ {
253
+ type: 'input',
254
+ name: 'version',
255
+ message: `Custom Update Version(${this.packageName}@${version}):`,
256
+ default: '',
257
+ validate: (answer) => {
258
+ if (!(/\d+.\d+.\d+/.test(answer))) {
259
+ return 'Version Should Be Like x.x.x';
260
+ }
261
+ if (answer === version) {
262
+ return 'Version Should Be Diff Than Before';
263
+ }
264
+ return true;
265
+ }
266
+ }
267
+ ]);
268
+ newVersion = result.version;
269
+ }
270
+ }
271
+ else {
272
+ const intersection = [
273
+ commandOptions.major && 'major',
274
+ commandOptions.minor && 'minor',
275
+ commandOptions.patch && 'patch'
276
+ ].filter(i => !!i);
277
+ if (intersection.length) {
278
+ newVersion = semver.inc(version, intersection[0]) || '';
279
+ }
280
+ else {
281
+ const types = new Set(commits.map(({ type }) => type));
282
+ const breaking = commits.some((commit) => !!commit.breaking);
283
+ const level = breaking
284
+ ? 'major'
285
+ : types.has('feat')
286
+ ? 'minor'
287
+ : 'patch';
288
+ newVersion = semver.inc(version, level) || '';
289
+ }
290
+ }
291
+ this.version = newVersion;
292
+ }
293
+ isChanged() {
294
+ return !!this.commits.length;
295
+ }
296
+ async updateCommits(commits, source) {
297
+ if (!commits.length)
298
+ return;
299
+ const { packageName } = this;
300
+ const olds = this.commits.map(i => JSON.stringify(i));
301
+ const newCommits = commits
302
+ .filter(i => {
303
+ return !olds.includes(JSON.stringify(i));
304
+ })
305
+ .map(j => {
306
+ return {
307
+ ...j,
308
+ effect: !!source
309
+ };
310
+ });
311
+ if (newCommits.length && this.commits.length) {
312
+ this.commits = this.commits.filter(i => !i.custom);
313
+ }
314
+ const commits$ = this.commits.concat(newCommits);
315
+ if (source) {
316
+ devShared.Logger.log(chalk.magenta(`MERGE COMMITS: `)
317
+ + chalk.bold(`${commits.length}`) + ` Commits. `
318
+ + 'merge ' + chalk.yellow(source) + ' into ' + chalk.green(packageName));
319
+ }
320
+ else {
321
+ devShared.Logger.log(``);
322
+ }
323
+ const changeLog = this.rebuildChangeLog(commits$);
324
+ if (changeLog) {
325
+ this.commits = commits$;
326
+ this.changeLog = changeLog;
327
+ }
328
+ else if (commits.length) {
329
+ devShared.Logger.log(chalk.red(`${commits.length} Commits Already Exists.`));
330
+ }
331
+ }
332
+ async updatePackageOptions(relationVerisons = {}) {
333
+ if (!this.isChanged())
334
+ return;
335
+ const { packageDir, packageOptions, commandOptions } = this;
336
+ const { dependencies, devDependencies } = packageOptions;
337
+ const newVersion = this.version;
338
+ devShared.Logger.log(chalk.yellow(`New Version: `) + `${newVersion}`);
339
+ packageOptions.version = newVersion;
340
+ if (Object.keys(this.packageRelation).length) {
341
+ for (let packageName$ in relationVerisons) {
342
+ let newVersion$ = relationVerisons[packageName$];
343
+ if (dependencies?.[packageName$]) {
344
+ dependencies[packageName$] = newVersion$;
345
+ }
346
+ if (devDependencies?.[packageName$]) {
347
+ devDependencies[packageName$] = newVersion$;
348
+ }
349
+ }
350
+ }
351
+ if (commandOptions.dryRun) {
352
+ devShared.Logger.log(chalk.yellow(`Skipping package.json Update`));
353
+ return;
354
+ }
355
+ devShared.Logger.log(chalk.yellow(`Updating `) + 'package.json');
356
+ fs.outputFileSync(`${packageDir}/package.json`, JSON.stringify(packageOptions, null, 2));
357
+ }
358
+ async updateChangelog() {
359
+ if (!this.isChanged())
360
+ return;
361
+ const { packageName, packageDir, packageOptions, commandOptions } = this;
362
+ const title = `# ${packageName} ChangeLog`;
363
+ const [date] = new Date().toISOString().split('T');
364
+ const logPath = path__namespace.resolve(packageDir, './CHANGELOG.md');
365
+ const logFile = fs.existsSync(logPath) ? fs.readFileSync(logPath, 'utf-8') : '';
366
+ const oldNotes = logFile.startsWith(title) ? logFile.slice(title.length).trim() : logFile;
367
+ const parts = [
368
+ `## v${packageOptions.version}`,
369
+ `_${date}_`,
370
+ this.changeLog
371
+ ].filter(Boolean);
372
+ const newLog = parts.join('\n\n');
373
+ if (commandOptions.dryRun) {
374
+ devShared.Logger.log(chalk.yellow(`New ChangeLog:`) + `\n${newLog}`);
375
+ return;
376
+ }
377
+ devShared.Logger.log(chalk.yellow(`Updating `) + `CHANGELOG.md`);
378
+ let content = [title, newLog, oldNotes].filter(Boolean).join('\n\n');
379
+ if (!content.endsWith('\n'))
380
+ content += '\n';
381
+ fs.writeFileSync(logPath, content, 'utf-8');
382
+ }
383
+ async test() {
384
+ if (!this.isChanged())
385
+ return;
386
+ const { commandOptions } = this;
387
+ if (commandOptions.dryRun) {
388
+ devShared.Logger.log(chalk.yellow('Skipping Test'));
389
+ return;
390
+ }
391
+ else {
392
+ devShared.Logger.log(chalk.yellow('Test...'));
393
+ }
394
+ await devShared.Shell.exec(`npm run test -- --package-name ${this.packageName}`);
395
+ }
396
+ async build() {
397
+ if (!this.isChanged())
398
+ return;
399
+ const { commandOptions } = this;
400
+ if (commandOptions.dryRun) {
401
+ devShared.Logger.log(chalk.yellow('Skipping Build'));
402
+ return;
403
+ }
404
+ else {
405
+ devShared.Logger.log(chalk.yellow('Build...'));
406
+ }
407
+ await devShared.Shell.exec(`npm run build -- --package-name ${this.packageName}`);
408
+ }
409
+ async publish() {
410
+ if (!this.isChanged())
411
+ return;
412
+ const { commandOptions, packageDir } = this;
413
+ if (commandOptions.dryRun || !commandOptions.publish) {
414
+ devShared.Logger.log(chalk.yellow(`Skipping Publish`));
415
+ return;
416
+ }
417
+ devShared.Logger.log(chalk.cyan(`\n Publishing to NPM`));
418
+ await devShared.Shell.spawn('npm', ['publish', '--no-git-checks', '--access', 'public'], {
419
+ cwd: packageDir
420
+ });
421
+ }
422
+ async tag() {
423
+ if (!this.isChanged())
424
+ return;
425
+ const { commandOptions, packageDir } = this;
426
+ const { packageName, packageOptions } = this;
427
+ if (commandOptions.dryRun || !commandOptions.tag) {
428
+ devShared.Logger.log(chalk.yellow(`Skipping Git Tag`));
429
+ return;
430
+ }
431
+ const tagName = `${packageName}@${packageOptions.version}`;
432
+ devShared.Logger.log(chalk.blue(`\n Tagging`) + chalk.grey(`${tagName}`));
433
+ await devShared.Shell.spawn('git', ['tag', tagName], {
434
+ cwd: packageDir
435
+ });
436
+ }
437
+ async process() {
438
+ const { workspace } = devShared.Locals.impl();
439
+ const { packageName, packageDir, packageFolderName } = this;
440
+ if (!packageDir || !fs.pathExists(packageDir)) {
441
+ throw new RangeError(`Could not find directory for package: ${packageFolderName}`);
442
+ }
443
+ devShared.Logger.log(chalk.cyan(`Releasing ${packageName}`) + ' from ' + chalk.grey(`${workspace}/${packageFolderName}`));
444
+ await this.parseCommits();
445
+ return this;
446
+ }
447
+ }
448
+ const release = (options, commandOptions) => {
449
+ return new Release(options, commandOptions);
450
+ };
451
+
452
+ const run = (options) => devShared.Utils.autoCatch(async () => {
453
+ options = {
454
+ dryRun: true,
455
+ tag: true,
456
+ publish: true,
457
+ commit: true,
458
+ push: true,
459
+ ...options
460
+ };
461
+ const locals = devShared.Locals.impl();
462
+ if (options.dryRun) {
463
+ devShared.Logger.log(chalk.magenta(`DRY RUN: `)
464
+ + 'No files will be modified.');
465
+ }
466
+ let inputs = [];
467
+ if (locals.workspace) {
468
+ inputs = locals.normalizePackageFolderNames;
469
+ }
470
+ else {
471
+ inputs = [''];
472
+ }
473
+ const instances = {};
474
+ await inputs
475
+ .reduce((preProcess, packageFolderName) => {
476
+ preProcess = preProcess
477
+ .then(() => release(packageFolderName, options).process())
478
+ .then((instance) => {
479
+ instances[packageFolderName] = instance;
480
+ });
481
+ return preProcess;
482
+ }, Promise.resolve());
483
+ devShared.Logger.log(chalk.blue(`---------------------\n`));
484
+ let message = `chore(release): publish\n\n`;
485
+ let relationVerisons = {};
486
+ await inputs.reduce((preProcess, packageFolderName) => {
487
+ const instance = instances[packageFolderName];
488
+ instance.packageRelation.forEach(i => {
489
+ let packageFolderName$ = devShared.Locals.getPackageFolderName(i);
490
+ let instance$ = instances[packageFolderName$];
491
+ if (instance$.commits.length > 0) {
492
+ instance.updateCommits(instance$.commits, instance$.packageName);
493
+ }
494
+ });
495
+ if (instance.commits.length) {
496
+ preProcess = preProcess
497
+ .then(() => devShared.Logger.log(chalk.magenta(`CHANGED: `) + instance.packageName))
498
+ .then(() => instance.test())
499
+ .then(() => instance.build())
500
+ .then(() => instance.updatePackageOptions(relationVerisons))
501
+ .then(() => instance.updateChangelog())
502
+ .then(() => {
503
+ message += `- ${instance.packageName}@${instance.packageOptions.version}\n`;
504
+ relationVerisons[instance.packageName] = `^${instance.packageOptions.version}`;
505
+ });
506
+ }
507
+ return preProcess;
508
+ }, Promise.resolve());
509
+ devShared.Logger.log(chalk.blue(`\n---------------------\n`));
510
+ const isChanged = Object.keys(relationVerisons).length;
511
+ if (!isChanged) {
512
+ devShared.Logger.log(chalk.magenta(`COMMIT: `) + 'Nothing Chanaged Found.');
513
+ }
514
+ else if (options.dryRun || !options.commit) {
515
+ devShared.Logger.log(chalk.magenta(`COMMIT: `) + chalk.yellow(`Skipping Git Commit`) + `\n${message}`);
516
+ }
517
+ else {
518
+ devShared.Logger.log(chalk.magenta(`CHANGED: `) + `pnpm-lock.yaml`);
519
+ await devShared.Shell.spawn('npx', ['pnpm', 'install', '--lockfile-only']);
520
+ devShared.Logger.log(chalk.magenta(`COMMIT: `) + `CHANGELOG.md, package.json, pnpm-lock.yaml`);
521
+ await devShared.Shell.spawn('git', ['add', process.cwd()]);
522
+ await devShared.Shell.spawn('git', ['commit', '--m', `'${message}'`]);
523
+ }
524
+ await inputs
525
+ .reduce((preProcess, packageFolderName) => {
526
+ const instance = instances[packageFolderName];
527
+ preProcess = preProcess
528
+ .then(() => instance.publish())
529
+ .then(() => instance.tag());
530
+ return preProcess;
531
+ }, Promise.resolve());
532
+ devShared.Logger.log(chalk.blue(`\n---------------------\n`));
533
+ if (options.dryRun || !options.push) {
534
+ devShared.Logger.log(chalk.magenta(`FINISH: `) + 'Skipping Git Push');
535
+ }
536
+ else if (!isChanged) {
537
+ devShared.Logger.log(chalk.magenta(`FINISH: `) + 'Nothing Chanaged.');
538
+ }
539
+ else {
540
+ await devShared.Shell.spawn('git', ['push']);
541
+ await devShared.Shell.spawn('git', ['push', '--tags']);
542
+ }
543
+ if (options.dryRun) {
544
+ devShared.Logger.log(chalk.green('NO DRY RUN WAY: ')
545
+ + chalk.grey(`npm run release -- --no-dry-run\n`));
546
+ }
547
+ }, {
548
+ onError: (e) => {
549
+ if (typeof e === 'number' && e === 1) {
550
+ devShared.Logger.error('发布失败');
551
+ }
552
+ else {
553
+ devShared.Logger.error(e);
554
+ }
555
+ process.exit(1);
556
+ }
557
+ });
558
+
559
+ exports.run = run;
@@ -0,0 +1,5 @@
1
+ import type { Options } from '@deot/dev-shared';
2
+
3
+ export declare const run: (options: Options) => Promise<any>;
4
+
5
+ export { }
@@ -0,0 +1,538 @@
1
+ import chalk from 'chalk';
2
+ import { Locals, Shell, Logger, Utils } from '@deot/dev-shared';
3
+ import * as path from 'node:path';
4
+ import fs from 'fs-extra';
5
+ import { createRequire } from 'node:module';
6
+ import parser from 'conventional-commits-parser';
7
+ import semver from 'semver';
8
+ import inquirer from 'inquirer';
9
+
10
+ const cwd = process.cwd();
11
+ const require$ = createRequire(cwd);
12
+ const { prompt } = inquirer;
13
+ const HASH = '-hash-';
14
+ const SUFFIX = '🐒💨🙊';
15
+ const parserOptions = {
16
+ noteKeywords: ['BREAKING CHANGE', 'Breaking Change']
17
+ };
18
+ const reBreaking = new RegExp(`(${parserOptions.noteKeywords.join(')|(')})`);
19
+ class Release {
20
+ packageDir;
21
+ packageName;
22
+ packageFolderName;
23
+ packageOptions;
24
+ packageRelation;
25
+ config;
26
+ changeLog;
27
+ version;
28
+ commits;
29
+ commandOptions;
30
+ constructor(config, commandOptions) {
31
+ const { packageDir, packageRelation } = Locals.impl();
32
+ if (typeof config === 'string') {
33
+ let packageFolderName = config;
34
+ let packageDir$ = path.resolve(packageDir, packageFolderName);
35
+ config = {
36
+ dir: packageDir$,
37
+ name: packageFolderName
38
+ };
39
+ }
40
+ this.packageDir = config.dir;
41
+ this.packageName = Locals.getPackageName(config.name);
42
+ this.packageFolderName = config.name;
43
+ this.packageOptions = require$(`${this.packageDir}/package.json`);
44
+ this.packageRelation = packageRelation[this.packageName] || [];
45
+ this.config = config;
46
+ this.commits = [];
47
+ this.changeLog = '';
48
+ this.version = '';
49
+ this.commandOptions = commandOptions;
50
+ }
51
+ async parseCommits() {
52
+ const { workspace } = Locals.impl();
53
+ const { packageFolderName, packageName, commandOptions } = this;
54
+ let params = ['tag', '--list', `'${packageName}@*'`, '--sort', '-v:refname'];
55
+ const { stdout: tags } = await Shell.exec('git', params);
56
+ const [latestTag] = tags.split('\n');
57
+ Logger.log(chalk.yellow(`Last Release Tag`) + `: ${latestTag || '<none>'}`);
58
+ params = ['--no-pager', 'log', `${latestTag}..HEAD`, `--format=%B%n${HASH}%n%H${SUFFIX}`];
59
+ let { stdout } = await Shell.exec('git', params);
60
+ let skipGetLog = false;
61
+ if (latestTag) {
62
+ const log1 = await Shell.exec('git', ['rev-parse', latestTag]);
63
+ const log2 = await Shell.exec('git', ['--no-pager', 'log', '-1', '--format=%H']);
64
+ if (log1.stdout === log2.stdout) {
65
+ skipGetLog = true;
66
+ }
67
+ }
68
+ if (!skipGetLog && !stdout) {
69
+ if (latestTag) {
70
+ params.splice(2, 1, `${latestTag}`);
71
+ }
72
+ else {
73
+ params.splice(2, 1, 'HEAD');
74
+ }
75
+ ({ stdout } = await Shell.exec('git', params));
76
+ }
77
+ const allowTypes = ['feat', `fix`, `break change`, `style`, `perf`, `types`, `refactor`, `chore`];
78
+ const rePlugin = new RegExp(`^(${allowTypes.join('|')})${workspace ? `\\(${packageFolderName}\\)` : '(\\(.+\\))?'}: .*`, 'i');
79
+ const allCommits = stdout.split(SUFFIX);
80
+ const commits = allCommits
81
+ .filter((commit) => {
82
+ const chunk = commit.trim();
83
+ return chunk && rePlugin.test(chunk);
84
+ })
85
+ .map((commit) => {
86
+ const node = parser.sync(commit);
87
+ const body = (node.body || node.footer);
88
+ if (!node.type)
89
+ node.type = parser.sync(node.header?.replace(/\(.+\)!?:/, ':') || '').type;
90
+ if (!node.hash)
91
+ node.hash = commit.split(HASH).pop()?.trim();
92
+ node.breaking = reBreaking.test(body) || /!:/.test(node.header);
93
+ node.effect = false;
94
+ node.custom = false;
95
+ return node;
96
+ });
97
+ if (!commits.length) {
98
+ Logger.log(chalk.red(`No Commits Found.`));
99
+ }
100
+ else {
101
+ Logger.log(chalk.yellow(`Found `)
102
+ + chalk.bold(`${allCommits.length}`)
103
+ + ` Commits, `
104
+ + chalk.bold(`${commits.length}`)
105
+ + ' Commits Valid');
106
+ }
107
+ const { skipUpdatePackage } = commandOptions;
108
+ if (commits.length && skipUpdatePackage) {
109
+ let skip = false;
110
+ if (typeof skipUpdatePackage === 'boolean' && skipUpdatePackage) {
111
+ let result = await prompt([
112
+ {
113
+ type: 'confirm',
114
+ name: 'skip',
115
+ message: `Skip Update(${this.packageName}@${this.packageOptions.version}):`,
116
+ default: true
117
+ }
118
+ ]);
119
+ skip = result.skip;
120
+ }
121
+ else if (typeof skipUpdatePackage === 'string'
122
+ && (skipUpdatePackage === '**'
123
+ || skipUpdatePackage.split(',').includes(this.packageName))) {
124
+ skip = true;
125
+ }
126
+ if (skip) {
127
+ Logger.log(chalk.red(`Skipping Update\n`));
128
+ return;
129
+ }
130
+ }
131
+ await this.updateVersion();
132
+ await this.updateCommits(commits);
133
+ const { forceUpdatePackage } = commandOptions;
134
+ if (!commits.length && forceUpdatePackage) {
135
+ let force = false;
136
+ if (typeof forceUpdatePackage === 'boolean' && forceUpdatePackage) {
137
+ let result = await prompt([
138
+ {
139
+ type: 'confirm',
140
+ name: 'force',
141
+ message: `Force Update(${this.packageName}@${this.packageOptions.version}):`,
142
+ default: true
143
+ }
144
+ ]);
145
+ force = result.force;
146
+ }
147
+ else if (typeof forceUpdatePackage === 'string'
148
+ && (forceUpdatePackage === '**'
149
+ || forceUpdatePackage.split(',').includes(this.packageName))) {
150
+ force = true;
151
+ }
152
+ if (force) {
153
+ const oldVersion = this.packageOptions.version;
154
+ const versionChanged = `\`${oldVersion}\` -> \`${this.version}\``;
155
+ this.commits = [
156
+ {
157
+ type: 'chore',
158
+ header: `chore(${this.packageFolderName || 'release'}): force-publish ${versionChanged}`,
159
+ hash: '',
160
+ effect: false,
161
+ breaking: false,
162
+ custom: true
163
+ }
164
+ ];
165
+ this.changeLog = `### Force Update Package\n\n- ${versionChanged}`.trim();
166
+ }
167
+ }
168
+ }
169
+ rebuildChangeLog(commits) {
170
+ const { packageDir } = this;
171
+ const { homepage, workspace } = Locals.impl();
172
+ const logPath = path.resolve(packageDir, './CHANGELOG.md');
173
+ const logFile = fs.existsSync(logPath) ? fs.readFileSync(logPath, 'utf-8') : '';
174
+ const notes = {
175
+ breaking: [],
176
+ features: [],
177
+ fixes: [],
178
+ updates: []
179
+ };
180
+ const closeRegxp = /\(?(closes? )\(?#((\d+))\)/ig;
181
+ const pullRegxp = /(?<!closes? )\((#(\d+))\)/ig;
182
+ for (const commit of commits) {
183
+ const { effect, breaking, hash, header, type } = commit;
184
+ const ref = !hash || pullRegxp.test(header)
185
+ ? ''
186
+ : ` ([${hash?.substring(0, 7)}](${homepage}/commit/${hash}))`;
187
+ let message = header?.trim();
188
+ if (workspace && !effect) {
189
+ message = message.replace(/\(.+\)!?:/, ':');
190
+ }
191
+ message = message
192
+ .replace(pullRegxp, `[$1](${homepage}/pull/$2)`)
193
+ .replace(closeRegxp, `[$1$2](${homepage}/issues/$2)`) + ref;
194
+ if (breaking) {
195
+ notes.breaking.push(message);
196
+ }
197
+ else if (type === 'fix') {
198
+ notes.fixes.push(message);
199
+ }
200
+ else if (type === 'feat') {
201
+ notes.features.push(message);
202
+ }
203
+ else {
204
+ notes.updates.push(message);
205
+ }
206
+ }
207
+ Object.keys(notes).forEach(i => {
208
+ notes[i] = notes[i].filter((j) => {
209
+ return !logFile.includes(j);
210
+ });
211
+ });
212
+ const parts = [
213
+ notes.breaking.length ? `### Breaking Changes\n\n- ${notes.breaking.join('\n- ')}`.trim() : '',
214
+ notes.fixes.length ? `### Bugfixes\n\n- ${notes.fixes.join('\n- ')}`.trim() : '',
215
+ notes.features.length ? `### Features\n\n- ${notes.features.join('\n- ')}`.trim() : '',
216
+ notes.updates.length ? `### Updates\n\n- ${notes.updates.join('\n- ')}`.trim() : ''
217
+ ].filter(Boolean);
218
+ const newLog = parts.join('\n\n');
219
+ return !parts.length || logFile.includes(newLog)
220
+ ? ''
221
+ : newLog;
222
+ }
223
+ async updateVersion() {
224
+ const { packageOptions, commits, commandOptions } = this;
225
+ const { version } = packageOptions;
226
+ let newVersion = '';
227
+ if (commandOptions.customVersion) {
228
+ newVersion = commandOptions.customVersion;
229
+ if (!(/\d+.\d+.\d+/.test(newVersion)) || version === newVersion) {
230
+ let result = await prompt([
231
+ {
232
+ type: 'input',
233
+ name: 'version',
234
+ message: `Custom Update Version(${this.packageName}@${version}):`,
235
+ default: '',
236
+ validate: (answer) => {
237
+ if (!(/\d+.\d+.\d+/.test(answer))) {
238
+ return 'Version Should Be Like x.x.x';
239
+ }
240
+ if (answer === version) {
241
+ return 'Version Should Be Diff Than Before';
242
+ }
243
+ return true;
244
+ }
245
+ }
246
+ ]);
247
+ newVersion = result.version;
248
+ }
249
+ }
250
+ else {
251
+ const intersection = [
252
+ commandOptions.major && 'major',
253
+ commandOptions.minor && 'minor',
254
+ commandOptions.patch && 'patch'
255
+ ].filter(i => !!i);
256
+ if (intersection.length) {
257
+ newVersion = semver.inc(version, intersection[0]) || '';
258
+ }
259
+ else {
260
+ const types = new Set(commits.map(({ type }) => type));
261
+ const breaking = commits.some((commit) => !!commit.breaking);
262
+ const level = breaking
263
+ ? 'major'
264
+ : types.has('feat')
265
+ ? 'minor'
266
+ : 'patch';
267
+ newVersion = semver.inc(version, level) || '';
268
+ }
269
+ }
270
+ this.version = newVersion;
271
+ }
272
+ isChanged() {
273
+ return !!this.commits.length;
274
+ }
275
+ async updateCommits(commits, source) {
276
+ if (!commits.length)
277
+ return;
278
+ const { packageName } = this;
279
+ const olds = this.commits.map(i => JSON.stringify(i));
280
+ const newCommits = commits
281
+ .filter(i => {
282
+ return !olds.includes(JSON.stringify(i));
283
+ })
284
+ .map(j => {
285
+ return {
286
+ ...j,
287
+ effect: !!source
288
+ };
289
+ });
290
+ if (newCommits.length && this.commits.length) {
291
+ this.commits = this.commits.filter(i => !i.custom);
292
+ }
293
+ const commits$ = this.commits.concat(newCommits);
294
+ if (source) {
295
+ Logger.log(chalk.magenta(`MERGE COMMITS: `)
296
+ + chalk.bold(`${commits.length}`) + ` Commits. `
297
+ + 'merge ' + chalk.yellow(source) + ' into ' + chalk.green(packageName));
298
+ }
299
+ else {
300
+ Logger.log(``);
301
+ }
302
+ const changeLog = this.rebuildChangeLog(commits$);
303
+ if (changeLog) {
304
+ this.commits = commits$;
305
+ this.changeLog = changeLog;
306
+ }
307
+ else if (commits.length) {
308
+ Logger.log(chalk.red(`${commits.length} Commits Already Exists.`));
309
+ }
310
+ }
311
+ async updatePackageOptions(relationVerisons = {}) {
312
+ if (!this.isChanged())
313
+ return;
314
+ const { packageDir, packageOptions, commandOptions } = this;
315
+ const { dependencies, devDependencies } = packageOptions;
316
+ const newVersion = this.version;
317
+ Logger.log(chalk.yellow(`New Version: `) + `${newVersion}`);
318
+ packageOptions.version = newVersion;
319
+ if (Object.keys(this.packageRelation).length) {
320
+ for (let packageName$ in relationVerisons) {
321
+ let newVersion$ = relationVerisons[packageName$];
322
+ if (dependencies?.[packageName$]) {
323
+ dependencies[packageName$] = newVersion$;
324
+ }
325
+ if (devDependencies?.[packageName$]) {
326
+ devDependencies[packageName$] = newVersion$;
327
+ }
328
+ }
329
+ }
330
+ if (commandOptions.dryRun) {
331
+ Logger.log(chalk.yellow(`Skipping package.json Update`));
332
+ return;
333
+ }
334
+ Logger.log(chalk.yellow(`Updating `) + 'package.json');
335
+ fs.outputFileSync(`${packageDir}/package.json`, JSON.stringify(packageOptions, null, 2));
336
+ }
337
+ async updateChangelog() {
338
+ if (!this.isChanged())
339
+ return;
340
+ const { packageName, packageDir, packageOptions, commandOptions } = this;
341
+ const title = `# ${packageName} ChangeLog`;
342
+ const [date] = new Date().toISOString().split('T');
343
+ const logPath = path.resolve(packageDir, './CHANGELOG.md');
344
+ const logFile = fs.existsSync(logPath) ? fs.readFileSync(logPath, 'utf-8') : '';
345
+ const oldNotes = logFile.startsWith(title) ? logFile.slice(title.length).trim() : logFile;
346
+ const parts = [
347
+ `## v${packageOptions.version}`,
348
+ `_${date}_`,
349
+ this.changeLog
350
+ ].filter(Boolean);
351
+ const newLog = parts.join('\n\n');
352
+ if (commandOptions.dryRun) {
353
+ Logger.log(chalk.yellow(`New ChangeLog:`) + `\n${newLog}`);
354
+ return;
355
+ }
356
+ Logger.log(chalk.yellow(`Updating `) + `CHANGELOG.md`);
357
+ let content = [title, newLog, oldNotes].filter(Boolean).join('\n\n');
358
+ if (!content.endsWith('\n'))
359
+ content += '\n';
360
+ fs.writeFileSync(logPath, content, 'utf-8');
361
+ }
362
+ async test() {
363
+ if (!this.isChanged())
364
+ return;
365
+ const { commandOptions } = this;
366
+ if (commandOptions.dryRun) {
367
+ Logger.log(chalk.yellow('Skipping Test'));
368
+ return;
369
+ }
370
+ else {
371
+ Logger.log(chalk.yellow('Test...'));
372
+ }
373
+ await Shell.exec(`npm run test -- --package-name ${this.packageName}`);
374
+ }
375
+ async build() {
376
+ if (!this.isChanged())
377
+ return;
378
+ const { commandOptions } = this;
379
+ if (commandOptions.dryRun) {
380
+ Logger.log(chalk.yellow('Skipping Build'));
381
+ return;
382
+ }
383
+ else {
384
+ Logger.log(chalk.yellow('Build...'));
385
+ }
386
+ await Shell.exec(`npm run build -- --package-name ${this.packageName}`);
387
+ }
388
+ async publish() {
389
+ if (!this.isChanged())
390
+ return;
391
+ const { commandOptions, packageDir } = this;
392
+ if (commandOptions.dryRun || !commandOptions.publish) {
393
+ Logger.log(chalk.yellow(`Skipping Publish`));
394
+ return;
395
+ }
396
+ Logger.log(chalk.cyan(`\n Publishing to NPM`));
397
+ await Shell.spawn('npm', ['publish', '--no-git-checks', '--access', 'public'], {
398
+ cwd: packageDir
399
+ });
400
+ }
401
+ async tag() {
402
+ if (!this.isChanged())
403
+ return;
404
+ const { commandOptions, packageDir } = this;
405
+ const { packageName, packageOptions } = this;
406
+ if (commandOptions.dryRun || !commandOptions.tag) {
407
+ Logger.log(chalk.yellow(`Skipping Git Tag`));
408
+ return;
409
+ }
410
+ const tagName = `${packageName}@${packageOptions.version}`;
411
+ Logger.log(chalk.blue(`\n Tagging`) + chalk.grey(`${tagName}`));
412
+ await Shell.spawn('git', ['tag', tagName], {
413
+ cwd: packageDir
414
+ });
415
+ }
416
+ async process() {
417
+ const { workspace } = Locals.impl();
418
+ const { packageName, packageDir, packageFolderName } = this;
419
+ if (!packageDir || !fs.pathExists(packageDir)) {
420
+ throw new RangeError(`Could not find directory for package: ${packageFolderName}`);
421
+ }
422
+ Logger.log(chalk.cyan(`Releasing ${packageName}`) + ' from ' + chalk.grey(`${workspace}/${packageFolderName}`));
423
+ await this.parseCommits();
424
+ return this;
425
+ }
426
+ }
427
+ const release = (options, commandOptions) => {
428
+ return new Release(options, commandOptions);
429
+ };
430
+
431
+ const run = (options) => Utils.autoCatch(async () => {
432
+ options = {
433
+ dryRun: true,
434
+ tag: true,
435
+ publish: true,
436
+ commit: true,
437
+ push: true,
438
+ ...options
439
+ };
440
+ const locals = Locals.impl();
441
+ if (options.dryRun) {
442
+ Logger.log(chalk.magenta(`DRY RUN: `)
443
+ + 'No files will be modified.');
444
+ }
445
+ let inputs = [];
446
+ if (locals.workspace) {
447
+ inputs = locals.normalizePackageFolderNames;
448
+ }
449
+ else {
450
+ inputs = [''];
451
+ }
452
+ const instances = {};
453
+ await inputs
454
+ .reduce((preProcess, packageFolderName) => {
455
+ preProcess = preProcess
456
+ .then(() => release(packageFolderName, options).process())
457
+ .then((instance) => {
458
+ instances[packageFolderName] = instance;
459
+ });
460
+ return preProcess;
461
+ }, Promise.resolve());
462
+ Logger.log(chalk.blue(`---------------------\n`));
463
+ let message = `chore(release): publish\n\n`;
464
+ let relationVerisons = {};
465
+ await inputs.reduce((preProcess, packageFolderName) => {
466
+ const instance = instances[packageFolderName];
467
+ instance.packageRelation.forEach(i => {
468
+ let packageFolderName$ = Locals.getPackageFolderName(i);
469
+ let instance$ = instances[packageFolderName$];
470
+ if (instance$.commits.length > 0) {
471
+ instance.updateCommits(instance$.commits, instance$.packageName);
472
+ }
473
+ });
474
+ if (instance.commits.length) {
475
+ preProcess = preProcess
476
+ .then(() => Logger.log(chalk.magenta(`CHANGED: `) + instance.packageName))
477
+ .then(() => instance.test())
478
+ .then(() => instance.build())
479
+ .then(() => instance.updatePackageOptions(relationVerisons))
480
+ .then(() => instance.updateChangelog())
481
+ .then(() => {
482
+ message += `- ${instance.packageName}@${instance.packageOptions.version}\n`;
483
+ relationVerisons[instance.packageName] = `^${instance.packageOptions.version}`;
484
+ });
485
+ }
486
+ return preProcess;
487
+ }, Promise.resolve());
488
+ Logger.log(chalk.blue(`\n---------------------\n`));
489
+ const isChanged = Object.keys(relationVerisons).length;
490
+ if (!isChanged) {
491
+ Logger.log(chalk.magenta(`COMMIT: `) + 'Nothing Chanaged Found.');
492
+ }
493
+ else if (options.dryRun || !options.commit) {
494
+ Logger.log(chalk.magenta(`COMMIT: `) + chalk.yellow(`Skipping Git Commit`) + `\n${message}`);
495
+ }
496
+ else {
497
+ Logger.log(chalk.magenta(`CHANGED: `) + `pnpm-lock.yaml`);
498
+ await Shell.spawn('npx', ['pnpm', 'install', '--lockfile-only']);
499
+ Logger.log(chalk.magenta(`COMMIT: `) + `CHANGELOG.md, package.json, pnpm-lock.yaml`);
500
+ await Shell.spawn('git', ['add', process.cwd()]);
501
+ await Shell.spawn('git', ['commit', '--m', `'${message}'`]);
502
+ }
503
+ await inputs
504
+ .reduce((preProcess, packageFolderName) => {
505
+ const instance = instances[packageFolderName];
506
+ preProcess = preProcess
507
+ .then(() => instance.publish())
508
+ .then(() => instance.tag());
509
+ return preProcess;
510
+ }, Promise.resolve());
511
+ Logger.log(chalk.blue(`\n---------------------\n`));
512
+ if (options.dryRun || !options.push) {
513
+ Logger.log(chalk.magenta(`FINISH: `) + 'Skipping Git Push');
514
+ }
515
+ else if (!isChanged) {
516
+ Logger.log(chalk.magenta(`FINISH: `) + 'Nothing Chanaged.');
517
+ }
518
+ else {
519
+ await Shell.spawn('git', ['push']);
520
+ await Shell.spawn('git', ['push', '--tags']);
521
+ }
522
+ if (options.dryRun) {
523
+ Logger.log(chalk.green('NO DRY RUN WAY: ')
524
+ + chalk.grey(`npm run release -- --no-dry-run\n`));
525
+ }
526
+ }, {
527
+ onError: (e) => {
528
+ if (typeof e === 'number' && e === 1) {
529
+ Logger.error('发布失败');
530
+ }
531
+ else {
532
+ Logger.error(e);
533
+ }
534
+ process.exit(1);
535
+ }
536
+ });
537
+
538
+ export { run };
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@deot/dev-releaser",
3
+ "version": "1.1.0",
4
+ "main": "dist/index.es.js",
5
+ "module": "dist/index.es.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "license": "MIT",
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "dependencies": {
16
+ "@deot/dev-extract": "^1.1.0",
17
+ "@deot/dev-shared": "^1.1.0",
18
+ "conventional-commits-parser": "^3.2.4",
19
+ "semver": "^7.3.8"
20
+ }
21
+ }