main
js 237 lines 7.37 KB
Raw
1 #!/usr/bin/env node
2 /**
3 * Copyright (c) Meta Platforms, Inc. and affiliates.
4 *
5 * This source code is licensed under the MIT license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9 'use strict';
10
11 const ora = require('ora');
12 const path = require('path');
13 const yargs = require('yargs');
14 const {hashElement} = require('folder-hash');
15 const promptForOTP = require('./prompt-for-otp');
16 const {PUBLISHABLE_PACKAGES} = require('./shared/packages');
17 const {
18 execHelper,
19 getDateStringForCommit,
20 spawnHelper,
21 } = require('./shared/utils');
22 const {buildPackages} = require('./shared/build-packages');
23 const {readJson, writeJson} = require('fs-extra');
24
25 /**
26 * Script for publishing PUBLISHABLE_PACKAGES to npm. By default, this runs in tarball mode, meaning
27 * the script will only print out what the contents of the files included in the npm tarball would
28 * be.
29 *
30 * Please run this first (ie `yarn npm:publish`) and double check the contents of the files that
31 * will be pushed to npm.
32 *
33 * If it looks good, you can run `yarn npm:publish --for-real` to really publish to npm. You must
34 * have 2FA enabled first and the script will prompt you to enter a 2FA code before proceeding.
35 * There's a small annoying delay before the packages are actually pushed to give you time to panic
36 * cancel. In this mode, we will bump the version field of each package's package.json, and git
37 * commit it. Then, the packages will be published to npm.
38 *
39 * Optionally, you can add the `--debug` flag to `yarn npm:publish --debug --for-real` to run all
40 * steps, but the final npm publish step will have the `--dry-run` flag added to it. This will make
41 * the command only report what it would have done, instead of actually publishing to npm.
42 */
43 async function main() {
44 const argv = yargs(process.argv.slice(2))
45 .option('packages', {
46 description: 'which packages to publish, defaults to all',
47 choices: PUBLISHABLE_PACKAGES,
48 default: PUBLISHABLE_PACKAGES,
49 })
50 .option('for-real', {
51 alias: 'frfr',
52 description:
53 'whether to publish to npm (npm publish) or dryrun (npm publish --dry-run)',
54 type: 'boolean',
55 default: false,
56 })
57 .option('debug', {
58 description:
59 'If enabled, will always run npm commands in dry run mode irregardless of the for-real flag',
60 type: 'boolean',
61 default: false,
62 })
63 .option('ci', {
64 description: 'Publish packages via CI',
65 type: 'boolean',
66 default: false,
67 })
68 .option('tag', {
69 description: 'Tag to publish to npm',
70 type: 'choices',
71 choices: ['experimental', 'beta', 'rc', 'latest'],
72 default: 'experimental',
73 })
74 .option('tag-version', {
75 description:
76 'Optional tag version to append to tag name, eg `1` becomes 0.0.0-rc.1',
77 type: 'number',
78 default: null,
79 })
80 .option('version-name', {
81 description: 'Version name',
82 type: 'string',
83 default: '0.0.0',
84 })
85 .help('help')
86 .strict()
87 .parseSync();
88
89 if (argv.debug === false) {
90 const currBranchName = await execHelper('git rev-parse --abbrev-ref HEAD');
91 const isPristine = (await execHelper('git status --porcelain')) === '';
92 if (currBranchName !== 'main' || isPristine === false) {
93 throw new Error(
94 'This script must be run from the `main` branch with no uncommitted changes'
95 );
96 }
97 }
98
99 let pkgNames = argv.packages;
100 if (Array.isArray(argv.packages) === false) {
101 pkgNames = [argv.packages];
102 }
103 const spinner = ora(
104 `Preparing to publish ${argv.versionName}@${argv.tag} ${
105 argv.forReal === true ? '(for real)' : '(dry run)'
106 } [debug=${argv.debug}]`
107 ).info();
108
109 await buildPackages(pkgNames);
110
111 if (argv.forReal === false) {
112 spinner.info('Dry run: Report tarball contents');
113 for (const pkgName of pkgNames) {
114 console.log(`\n========== ${pkgName} ==========\n`);
115 spinner.start(`Running npm pack --dry-run\n`);
116 try {
117 await spawnHelper('npm', ['pack', '--dry-run'], {
118 cwd: path.resolve(__dirname, `../../packages/${pkgName}`),
119 stdio: 'inherit',
120 });
121 } catch (e) {
122 spinner.fail(e.toString());
123 throw e;
124 }
125 spinner.stop(`Successfully packed ${pkgName} (dry run)`);
126 }
127 spinner.succeed(
128 'Please confirm contents of packages before publishing. You can run this command again with --for-real to publish to npm'
129 );
130 }
131
132 if (argv.forReal === true) {
133 const commit = await execHelper(
134 'git show -s --no-show-signature --format=%h',
135 {
136 cwd: path.resolve(__dirname, '..'),
137 }
138 );
139 const dateString = await getDateStringForCommit(commit);
140 const otp =
141 argv.ci === false && argv.debug === false ? await promptForOTP() : null;
142 const {hash} = await hashElement(path.resolve(__dirname, '../..'), {
143 encoding: 'hex',
144 folders: {exclude: ['node_modules']},
145 files: {exclude: ['.DS_Store']},
146 });
147 const truncatedHash = hash.slice(0, 7);
148 let newVersion;
149 if (argv.tag === 'latest') {
150 newVersion = argv.versionName;
151 } else {
152 newVersion =
153 argv.tagVersion == null || argv.tagVersion === ''
154 ? `${argv.versionName}-${argv.tag}`
155 : `${argv.versionName}-${argv.tag}.${argv.tagVersion}`;
156 }
157 if (argv.tag === 'experimental' || argv.tag === 'beta') {
158 newVersion = `${newVersion}-${truncatedHash}-${dateString}`;
159 }
160
161 for (const pkgName of pkgNames) {
162 const pkgDir = path.resolve(__dirname, `../../packages/${pkgName}`);
163 const pkgJsonPath = path.resolve(
164 __dirname,
165 `../../packages/${pkgName}/package.json`
166 );
167
168 spinner.start(`Writing package.json for ${pkgName}@${newVersion}`);
169 await writeJson(
170 pkgJsonPath,
171 {
172 ...(await readJson(pkgJsonPath)),
173 version: newVersion,
174 },
175 {spaces: 2}
176 );
177 spinner.succeed(`Wrote package.json for ${pkgName}@${newVersion}`);
178
179 console.log(`\n========== ${pkgName} ==========\n`);
180 spinner.start(`Publishing ${pkgName}@${newVersion} to npm\n`);
181
182 let opts = [];
183 if (argv.debug === true) {
184 opts.push('--dry-run');
185 }
186 if (otp != null) {
187 opts.push(`--otp=${otp}`);
188 }
189
190 opts.push(`--tag=${argv.tag}`);
191
192 try {
193 await spawnHelper(
194 'npm',
195 ['publish', ...opts, '--registry=https://registry.npmjs.org'],
196 {
197 cwd: pkgDir,
198 stdio: 'inherit',
199 }
200 );
201 console.log('\n');
202 } catch (e) {
203 spinner.fail(e.toString());
204 throw e;
205 }
206 spinner.succeed(`Successfully published ${pkgName} to npm`);
207
208 spinner.start('Pushing tags to npm');
209 if (typeof argv.tag === 'string') {
210 try {
211 let opts = ['dist-tag', 'add', `${pkgName}@${newVersion}`, argv.tag];
212 if (otp != null) {
213 opts.push(`--otp=${otp}`);
214 }
215 if (argv.debug === true) {
216 spinner.info(`dry-run: npm ${opts.join(' ')}`);
217 } else {
218 await spawnHelper('npm', opts, {
219 cwd: pkgDir,
220 stdio: 'inherit',
221 });
222 }
223 } catch (e) {
224 spinner.fail(e.toString());
225 throw e;
226 }
227 spinner.succeed(
228 `Successfully pushed dist-tag ${argv.tag} for ${pkgName} to npm`
229 );
230 }
231 }
232
233 console.log('\n\n✅ All done');
234 }
235 }
236
237 main();