| 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(); |