UNPKG

6.84 kBJavaScriptView Raw
1'use strict';
2require('any-observable/register/rxjs-all'); // eslint-disable-line import/no-unassigned-import
3const fs = require('fs');
4const path = require('path');
5const execa = require('execa');
6const del = require('del');
7const Listr = require('listr');
8const split = require('split');
9const {merge, throwError} = require('rxjs');
10const {catchError, filter, finalize} = require('rxjs/operators');
11const streamToObservable = require('@samverschueren/stream-to-observable');
12const readPkgUp = require('read-pkg-up');
13const hasYarn = require('has-yarn');
14const pkgDir = require('pkg-dir');
15const hostedGitInfo = require('hosted-git-info');
16const onetime = require('onetime');
17const exitHook = require('async-exit-hook');
18const prerequisiteTasks = require('./prerequisite-tasks');
19const gitTasks = require('./git-tasks');
20const publish = require('./npm/publish');
21const enable2fa = require('./npm/enable-2fa');
22const npm = require('./npm/util');
23const releaseTaskHelper = require('./release-task-helper');
24const util = require('./util');
25const git = require('./git-util');
26
27const exec = (cmd, args) => {
28 // Use `Observable` support if merged https://github.com/sindresorhus/execa/pull/26
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
38module.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 // TODO: Remove sometime far in the future
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) { // Verify that the package's version has been bumped before deleting the last tag and commit.
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 // The default parameter is a workaround for https://github.com/Tapppi/async-exit-hook/issues/9
89 exitHook((callback = () => {}) => {
90 if (publishStatus === 'FAILED' && runPublish) {
91 (async () => {
92 await rollback();
93 callback();
94 })();
95 } else if (publishStatus === 'SUCCESS' && runPublish) {
96 // Do nothing
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};