#!/usr/bin/env node import * as yargs from 'yargs'; import { execSync } from 'child_process'; import { existsSync, readFileSync, writeFileSync } from 'fs'; import { URL } from 'url'; import { join } from 'path'; import * as version from '@lerna/version/index'; import * as publish from '@lerna/publish/index'; function hideFromGitIndex(uncommittedFiles: string[]) { execSync(`git update-index --assume-unchanged ${uncommittedFiles.join(' ')}`); return () => execSync( `git update-index --no-assume-unchanged ${uncommittedFiles.join(' ')}` ); } (async () => { const options = parseArgs(); if (!options.local && !options.force) { console.log('Authenticating to NPM'); execSync('npm adduser', { stdio: [0, 1, 2], }); } if (options.clearLocalRegistry) { execSync('yarn local-registry clear'); } const buildCommand = 'yarn build'; console.log(`> ${buildCommand}`); execSync(buildCommand, { stdio: [0, 1, 2], }); if (options.local) { // Force all projects to be not private const projects = JSON.parse( execSync('npx lerna list --json --all').toString() ); for (const proj of projects) { if (proj.private) { console.log('Publishing private package locally:', proj.name); const packageJsonPath = join(proj.location, 'package.json'); const original = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); writeFileSync( packageJsonPath, JSON.stringify({ ...original, private: false }) ); } } } const versionOptions = { bump: options.version ? options.version : undefined, conventionalCommits: true, conventionalPrerelease: options.tag === 'next', preid: options.preid, forcePublish: true, createRelease: options.tag !== 'next' ? 'github' : undefined, noChangelog: options.tag === 'next', tagVersionPrefix: '', exact: true, gitRemote: options.gitRemote, gitTagVersion: options.tag !== 'next', message: 'chore(misc): publish %v', loglevel: options.loglevel ?? 'info', yes: false, }; if (options.local) { versionOptions.conventionalCommits = false; delete versionOptions.createRelease; versionOptions.gitTagVersion = false; versionOptions.loglevel = options.loglevel ?? 'error'; versionOptions.yes = true; versionOptions.bump = options.version ? options.version : 'minor'; } const lernaJsonPath = join(__dirname, '../lerna.json'); let originalLernaJson: Buffer | undefined; if (options.local || options.tag === 'next') { originalLernaJson = readFileSync(lernaJsonPath); } if (options.local) { /** * Hide changes from Lerna */ const uncommittedFiles = execSync('git diff --name-only --relative HEAD .') .toString() .split('\n') .filter((i) => i.length > 0) .filter((f) => existsSync(f)); const unhideFromGitIndex = hideFromGitIndex(uncommittedFiles); process.on('exit', unhideFromGitIndex); process.on('SIGTERM', unhideFromGitIndex); process.on('SIGINT', unhideFromGitIndex); } const publishOptions: Record = { gitReset: false, distTag: options.tag, }; if (!options.skipPublish) { await publish({ ...versionOptions, ...publishOptions }); } else { await version(versionOptions); console.warn('Not Publishing because --dryRun was passed'); } if (originalLernaJson) { writeFileSync(lernaJsonPath, originalLernaJson); } })(); function parseArgs() { const parsedArgs = yargs .scriptName('yarn nx-release') .wrap(144) .strictOptions() .version(false) .command( '$0 [version]', 'This script is for publishing Nx both locally and publically' ) .option('skipPublish', { type: 'boolean', description: 'Skips the actual publishing for testing out versioning', }) .option('clearLocalRegistry', { type: 'boolean', description: 'Clear existing versions in the local registry so that you can republish the same version', default: true, }) .option('local', { type: 'boolean', description: 'Publish Nx locally, not to actual NPM', alias: 'l', default: true, }) .option('force', { type: 'boolean', description: "Don't use this unless you really know what it does", hidden: true, }) .positional('version', { type: 'string', description: 'The version to publish. This does not need to be passed and can be inferred.', }) .option('gitRemote', { type: 'string', description: 'Alternate git remote name to publish tags to (useful for testing changelog)', default: 'origin', }) .option('tag', { type: 'string', description: 'NPM Tag', choices: ['next', 'latest', 'previous'], }) .option('preid', { type: 'string', description: 'The kind of prerelease tag. (1.0.0-[preid].0)', choices: ['alpha', 'beta', 'rc'], default: 'beta', }) .option('loglevel', { type: 'string', description: 'Log Level', choices: ['error', 'info', 'debug'], }) .example( '$0', `By default, this will locally publish a minor version bump as latest. Great for local development. Most developers should only need this.` ) .example( '$0 --local false', `This will really publish a new beta version to npm as next. The version is inferred by the changes.` ) .example( '$0 --local false --tag latest', `This will really publish a new stable version to npm as latest, tag, commit, push, and create a release on GitHub.` ) .example( '$0 --local false --preid rc', `This will really publish a new rc version to npm as next.` ) .group( ['local', 'clearLocalRegistry'], 'Local Publishing Options for most developers' ) .group( ['preid', 'tag', 'gitRemote', 'force'], 'Real Publishing Options for actually publishing to NPM' ) .check((args) => { const registry = getRegistry(); const registryIsLocalhost = registry.hostname === 'localhost'; if (!args.local) { if (!process.env.GH_TOKEN) { throw new Error('process.env.GH_TOKEN is not set'); } if (!args.force && registryIsLocalhost) { throw new Error( 'Registry is still set to localhost! Run "yarn local-registry disable" or pass --force' ); } } else { if (!args.force && !registryIsLocalhost) { throw new Error('--local was passed and registry is not localhost'); } } return true; }) .parseSync(); parsedArgs.tag ??= parsedArgs.local ? 'latest' : 'next'; return parsedArgs; } function getRegistry() { return new URL(execSync('npm config get registry').toString().trim()); }