1 | 'use strict';
|
2 | require('any-observable/register/rxjs-all');
|
3 | const fs = require('fs');
|
4 | const path = require('path');
|
5 | const execa = require('execa');
|
6 | const del = require('del');
|
7 | const Listr = require('listr');
|
8 | const split = require('split');
|
9 | const {merge, throwError} = require('rxjs');
|
10 | const {catchError, filter, finalize} = require('rxjs/operators');
|
11 | const streamToObservable = require('@samverschueren/stream-to-observable');
|
12 | const readPkgUp = require('read-pkg-up');
|
13 | const hasYarn = require('has-yarn');
|
14 | const pkgDir = require('pkg-dir');
|
15 | const hostedGitInfo = require('hosted-git-info');
|
16 | const onetime = require('onetime');
|
17 | const exitHook = require('async-exit-hook');
|
18 | const prerequisiteTasks = require('./prerequisite-tasks');
|
19 | const gitTasks = require('./git-tasks');
|
20 | const publish = require('./npm/publish');
|
21 | const enable2fa = require('./npm/enable-2fa');
|
22 | const npm = require('./npm/util');
|
23 | const releaseTaskHelper = require('./release-task-helper');
|
24 | const util = require('./util');
|
25 | const git = require('./git-util');
|
26 |
|
27 | const exec = (cmd, args) => {
|
28 |
|
29 | const cp = execa(cmd, args);
|
30 |
|
31 | return merge(
|
32 | streamToObservable(cp.stdout.pipe(split())),
|
33 | streamToObservable(cp.stderr.pipe(split())),
|
34 | cp
|
35 | ).pipe(filter(Boolean));
|
36 | };
|
37 |
|
38 | module.exports = async (input = 'patch', options) => {
|
39 | options = {
|
40 | cleanup: true,
|
41 | tests: true,
|
42 | publish: true,
|
43 | ...options
|
44 | };
|
45 |
|
46 | if (!hasYarn() && options.yarn) {
|
47 | throw new Error('Could not use Yarn without yarn.lock file');
|
48 | }
|
49 |
|
50 |
|
51 | if (options.skipCleanup) {
|
52 | options.cleanup = false;
|
53 | }
|
54 |
|
55 | const pkg = util.readPkg();
|
56 | const runTests = options.tests && !options.yolo;
|
57 | const runCleanup = options.cleanup && !options.yolo;
|
58 | const runPublish = options.publish && !pkg.private;
|
59 | const pkgManager = options.yarn === true ? 'yarn' : 'npm';
|
60 | const pkgManagerName = options.yarn === true ? 'Yarn' : 'npm';
|
61 | const rootDir = pkgDir.sync();
|
62 | const hasLockFile = fs.existsSync(path.resolve(rootDir, options.yarn ? 'yarn.lock' : 'package-lock.json')) || fs.existsSync(path.resolve(rootDir, 'npm-shrinkwrap.json'));
|
63 | const isOnGitHub = options.repoUrl && (hostedGitInfo.fromUrl(options.repoUrl) || {}).type === 'github';
|
64 |
|
65 | let publishStatus = 'UNKNOWN';
|
66 |
|
67 | const rollback = onetime(async () => {
|
68 | console.log('\nPublish failed. Rolling back to the previous state…');
|
69 |
|
70 | const tagVersionPrefix = await util.getTagVersionPrefix(options);
|
71 |
|
72 | const latestTag = await git.latestTag();
|
73 | const versionInLatestTag = latestTag.slice(tagVersionPrefix.length);
|
74 |
|
75 | try {
|
76 | if (versionInLatestTag === util.readPkg().version &&
|
77 | versionInLatestTag !== pkg.version) {
|
78 | await git.deleteTag(latestTag);
|
79 | await git.removeLastCommit();
|
80 | }
|
81 |
|
82 | console.log('Successfully rolled back the project to its previous state.');
|
83 | } catch (error) {
|
84 | console.log(`Couldn't roll back because of the following error:\n${error}`);
|
85 | }
|
86 | });
|
87 |
|
88 |
|
89 | exitHook((callback = () => {}) => {
|
90 | if (publishStatus === 'FAILED' && runPublish) {
|
91 | (async () => {
|
92 | await rollback();
|
93 | callback();
|
94 | })();
|
95 | } else if (publishStatus === 'SUCCESS' && runPublish) {
|
96 |
|
97 | } else {
|
98 | console.log('\nAborted!');
|
99 | callback();
|
100 | }
|
101 | });
|
102 |
|
103 | const tasks = new Listr([
|
104 | {
|
105 | title: 'Prerequisite check',
|
106 | enabled: () => runPublish,
|
107 | task: () => prerequisiteTasks(input, pkg, options)
|
108 | },
|
109 | {
|
110 | title: 'Git',
|
111 | task: () => gitTasks(options)
|
112 | }
|
113 | ], {
|
114 | showSubtasks: false
|
115 | });
|
116 |
|
117 | if (runCleanup) {
|
118 | tasks.add([
|
119 | {
|
120 | title: 'Cleanup',
|
121 | skip: () => hasLockFile,
|
122 | task: () => del('node_modules')
|
123 | },
|
124 | {
|
125 | title: 'Installing dependencies using Yarn',
|
126 | enabled: () => options.yarn === true,
|
127 | task: () => exec('yarn', ['install', '--frozen-lockfile', '--production=false']).pipe(
|
128 | catchError(error => {
|
129 | if (error.stderr.startsWith('error Your lockfile needs to be updated')) {
|
130 | return throwError(new Error('yarn.lock file is outdated. Run yarn, commit the updated lockfile and try again.'));
|
131 | }
|
132 |
|
133 | return throwError(error);
|
134 | })
|
135 | )
|
136 | },
|
137 | {
|
138 | title: 'Installing dependencies using npm',
|
139 | enabled: () => options.yarn === false,
|
140 | task: () => {
|
141 | const args = hasLockFile ? ['ci'] : ['install', '--no-package-lock', '--no-production'];
|
142 | return exec('npm', args);
|
143 | }
|
144 | }
|
145 | ]);
|
146 | }
|
147 |
|
148 | if (runTests) {
|
149 | tasks.add([
|
150 | {
|
151 | title: 'Running tests using npm',
|
152 | enabled: () => options.yarn === false,
|
153 | task: () => exec('npm', ['test'])
|
154 | },
|
155 | {
|
156 | title: 'Running tests using Yarn',
|
157 | enabled: () => options.yarn === true,
|
158 | task: () => exec('yarn', ['test']).pipe(
|
159 | catchError(error => {
|
160 | if (error.message.includes('Command "test" not found')) {
|
161 | return [];
|
162 | }
|
163 |
|
164 | return throwError(error);
|
165 | })
|
166 | )
|
167 | }
|
168 | ]);
|
169 | }
|
170 |
|
171 | tasks.add([
|
172 | {
|
173 | title: 'Bumping version using Yarn',
|
174 | enabled: () => options.yarn === true,
|
175 | task: () => exec('yarn', ['version', '--new-version', input])
|
176 | },
|
177 | {
|
178 | title: 'Bumping version using npm',
|
179 | enabled: () => options.yarn === false,
|
180 | task: () => exec('npm', ['version', input])
|
181 | }
|
182 | ]);
|
183 |
|
184 | if (runPublish) {
|
185 | tasks.add([
|
186 | {
|
187 | title: `Publishing package using ${pkgManagerName}`,
|
188 | task: (context, task) => {
|
189 | let hasError = false;
|
190 |
|
191 | return publish(context, pkgManager, task, options)
|
192 | .pipe(
|
193 | catchError(async error => {
|
194 | hasError = true;
|
195 | await rollback();
|
196 | throw new Error(`Error publishing package:\n${error.message}\n\nThe project was rolled back to its previous state.`);
|
197 | }),
|
198 | finalize(() => {
|
199 | publishStatus = hasError ? 'FAILED' : 'SUCCESS';
|
200 | })
|
201 | );
|
202 | }
|
203 | }
|
204 | ]);
|
205 |
|
206 | const isExternalRegistry = npm.isExternalRegistry(pkg);
|
207 | if (!options.exists && !pkg.private && !isExternalRegistry) {
|
208 | tasks.add([
|
209 | {
|
210 | title: 'Enabling two-factor authentication',
|
211 | task: (context, task) => enable2fa(task, pkg.name, {otp: context.otp})
|
212 | }
|
213 | ]);
|
214 | }
|
215 | }
|
216 |
|
217 | tasks.add({
|
218 | title: 'Pushing tags',
|
219 | skip: async () => {
|
220 | if (!(await git.hasUpstream())) {
|
221 | return 'Upstream branch not found; not pushing.';
|
222 | }
|
223 |
|
224 | if (publishStatus === 'FAILED' && runPublish) {
|
225 | return 'Couldn\'t publish package to npm; not pushing.';
|
226 | }
|
227 | },
|
228 | task: () => git.push()
|
229 | });
|
230 |
|
231 | tasks.add({
|
232 | title: 'Creating release draft on GitHub',
|
233 | enabled: () => isOnGitHub === true,
|
234 | skip: () => !options.releaseDraft,
|
235 | task: () => releaseTaskHelper(options, pkg)
|
236 | });
|
237 |
|
238 | await tasks.run();
|
239 |
|
240 | const {package: newPkg} = await readPkgUp();
|
241 | return newPkg;
|
242 | };
|