Skip to content
/ lerna-ci Public

The essential toolkit for monorepo managed by lerna/yarn/pnpm/turbo/etc

License

Notifications You must be signed in to change notification settings

oe/lerna-ci

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

lerna-ci

The essential toolkit for monorepo managed by lerna/npm/yarn/pnpm/turbo/etc

Features

  • sync versions of packages in monorepo (with cli command)
  • sync(update) dependencies versions of all packages (with cli command)
    • can update to latest version
    • can update to specified version
    • can specify packages with wildcard characters
  • format all package.json files (with cli command)
  • check if all packages are qualified to publish to npm (with cli command)
  • list all packages(requires lerna or @changesets/cli) (with cli command)
  • get all packages' meta info
  • some other useful utilities

lerna-ci is designed for monorepo, but it can also be used in standard repo.

Install

# with yarn, install as a devDependency
yarn add lerna-ci -D

# with npm, install as a devDependency
npm install lerna-ci -D

you may also install it to global if you use cli commands frequently(not recommended)

Notice: lerna-ci requires node >=14.6

Usage

# sync versions of packages in monorepo, to fix versions of packages in monorepo when they are messed up
yarn lerna-ci synclocal

# sync all packages' dependencies versions
#   following command will sync all @babel scoped npm packages and typescript to latest version, but react and react-dom will be set to 16.x
yarn lerna-ci syncremote "@babel/*" "react@16.x" "react-dom@16.x" typescript

# format all package.json files
yarn lerna-ci fixpack

Cli commands

lerna-ci also provide some cli commands, so that you do some task with a single line code.

synclocal

sync versions of packages in monorepo, using syncLocal under the hood.

# with yarn
yarn lerna-ci synclocal [source] [--check-only]

# version source, determine where to get the packages' versions, could be: 
#   git, npm, local, or all, default local
# if check-only is true, it will only check if packages' versions are synced, exit 1 if not synced

# or if you prefer npm
npx lerna-ci synclocal [source] [--check-only]

# check for more options and examples
yarn lerna-ci synclocal --help

# demo
yarn lerna-ci synclocal

It's very useful when local packages versions are messed up, this may lead to some unexpected errors, it can happens in some cases:

  1. publish a beta version inside a package without using lerna(or other monorepo tools)
  2. partial success when publish packages with lerna(or other monorepo tools), you may use yarn lerna-ci synclocal all to fix it

You may need to run yarn or npm install to make your changes take effect.

syncdeps

sync all packages' dependencies versions in monorepo, using syncDeps under the hood.

It will update following kinds of dependencies:

  • dependencies
  • devDependencies
  • optionalDependencies
  • peerDependencies
# with yarn
yarn lerna-ci syncdeps <packageNames...> [--check-only]
# packageNames could be a list of package names, or a list of package name with version range, such as: 
#     "@babel/*" "@babel/core@^7.0.0" "parcel@^2.0.0" "rollup-plugin*"
#     package name with asterisk(*) must be quoted
# packageNames not found or not matched will be ignored
# if check-only is true, it will check if any package's dependencies need be synced, exit 1 if found

# or if you prefer npm, must use quotes when specify scoped wildcard package name
npx lerna-ci syncdeps <packageNames...> [--check-only]

# check for more options and examples
yarn lerna-ci syncdeps --help

# demo
## sync all babel related packages' versions to `^7.0.0`, all packages that start with `eslint-plugin-` to latest, and react react-dom to `18.2.0`
yarn lerna-ci syncdeps "@babel/*@^7.0.0" "eslint-plugin-*" react@18.2.0 react-dom@18.2.0

You may need to run yarn or npm install to make your changes take effect.

canpublish

check if all packages are qualified to publish to npm, using getChanged under the hood if lerna or @changesets/cli is available, it will check:

  1. whether local has uncommitted changes when in git repo if --check-git is true
  2. whether local has conflicts when in git repo
  3. whether local is behind of remote when in git repo
  4. whether local packages' versions are synced with the latest version when use-max-version is true
  5. whether next versions(can be configured via releaseType and period) of local packages are occupied on npm and git

it will exit 1 if any of the above conditions is satisfied

yarn lerna-ci canpublish [--releaseType=patch] [--period=alpha]
# releaseType: patch, minor, major, prepatch, preminor, premajor, prerelease
#              default patch
# period: a string like alpha, beta, rc, etc, default alpha, only available when releaseType is pre*

# check for more options and examples
yarn lerna-ci canpublish --help

# demo
# check if all (changed) packages are qualified to publish a beta version in patch
yarn lerna-ci canpublish --releaseType=patch --period=beta

fixpack

format all packages' package.json, using fixpack under the hood

# with yarn
yarn lerna-ci fixpack

# or if you prefer npm
npx lerna-ci fixpack

above command will format all package.json files with default configuration, you can configure fixpack's params via configuration file

changed

list all changed packages, using getChanged under the hood

# with yarn
yarn lerna-ci changed

# or if you prefer npm
npx lerna-ci changed

above command requires package lerna or @changesets/cli to be installed, or it will exit 1.

You should configure lerna or @changesets/cli following their documentations and manage your packages under their official guidelines, or this command will not work as expected.

API

getAllPackageDigests

get all packages info in monorepo(including the root package), you can filter with custom options

e.g.

import { getAllPackageDigests } from 'lerna-ci'

// get all packages
getAllPackageDigests().then(res => console.log(res))
// [{name: 'my-package', version: '1.0.1', private: false, location: '/Users/xx/work/monorepo/my-package'}]

// find packages with custom filter function
getAllPackageDigests((digest => digest.name.startWith('@inner/a-'))).then(res => console.log(res))

// find public packages which package name contains '@inner/'
getAllPackageDigests({ignorePrivate: true, keyword: '@inner/'}).then(res => console.log(res))

Type Declarations:

getAllPackageDigests(filter?: IPackageFilterOptions) => Promise<IPackageDigest[]>

/** package filter object */
export interface IPackageFilterObject {
  /** whether need private package */
  ignorePrivate?: boolean
  /** search package contains the keyword */
  keyword?: string
}
/** package filter function */
export type IPackageFilter = (pkg: IPackageDigest, index: number, arr: IPackageDigest[]) => boolean

export type IPackageFilterOptions = IPackageFilterObject | IPackageFilter

/**
 * package digest info
 */
export interface IPackageDigest {
  /** package name */
  name: string
  /** package version */
  version: string
  /** whether package is private */
  private: boolean
  /** package folder full path */
  location: string
}

syncLocal

sync versions of packages in monorepo(version info can be fetch from npm or git tag), if they depend each other and dependence version will be rematched.

also available as command synclocal

e.g.

import { syncLocal } from 'lerna-ci'
// return all changed package infos
const updatedPkgs = await syncLocal({ versionSource: 'npm' })
// [{name: 'my-package', version: '1.0.1', private: false, location: '/Users/xx/work/monorepo/my-package'}]

Type Declarations:

syncLocal(options?: ISyncPackageOptions) => Promise<IPackageDigest[]>


export interface ISyncPackageOptions {
  /**
   * version source, default to `local`
   * how to get latest locale package versions: npm, git, local or all
   * @default 'all'
   */
  versionSource?: EVerSource
  /**
   * npm/git version strategy
   * @default 'latest'
   */
  versionStrategy?: IVersionPickStrategy
  /**
   * filter which package should be synced
   */
  packageFilter?: IPackageFilterOptions
  /**
   * version range strategy
   * @default 'retain'
   */
  versionRangeStrategy?: IUpgradeVersionStrategy
  /**
   * only check, with package.json files untouched
   * validate package whether need to update, don't change package.json file actually
   */
  checkOnly?: boolean
  /**
   * check whether packages' versions are exactly same
   */
  exact?: boolean
}

/**
 * upgrade version strategy
 *  retain: retain the original version range
 */
export type IUpgradeVersionStrategy = '>' | '~' | '^' | '>=' | '' | 'retain' | IVerTransform

/**
 * custom version transform
 */
export type IVerTransform = (name: string, newVersion: string, oldVersion: string) => string

Tips: you may need to reinstall your workspace dependence if anything changed

syncDeps

sync packages dependencies(e.g. babel, react, typescript, etc) versions at once

also available as command syncdeps

import { syncDeps } from 'lerna-ci'

// update all packages that depend `react` and `react-dom` to their latest version(will fetch from npm)
//  return all changed package infos(aka all packages that depend on these packageNames and be updated)
const updatedPkgs = await syncDeps({ packageNames: ['react', 'react-dom'] })
// [{name: 'my-package', version: '1.0.1', private: false, location: '/Users/xx/work/monorepo/my-package'}]

// as above, but will also update dependence typescript to a fixed version 3.1.0 and parcel to ^2.0.0
const updatedPkgs = await syncDeps({ packageNames: ['react', 'react-dom'], versionMap: { typescript: '=3.1.0', parcel: '2.0.0' } })
// [{name: 'my-package', version: '1.0.1', private: false, location: '/Users/xx/work/monorepo/my-package'}]

Type Declarations:

syncDeps(syncOptions: ISyncDepOptions)=> Promise<IPackageDigest[]>

export interface ISyncDepOptions {
  /** 
   * package names that should update
   *  will fetch its version from npm by default
   *  package name can use asterisk, e.g. @babel/*
   * 
   * @example
   *  ['duplex-message', '@typescript-eslint/parser', '@babel/*', '*plugin*', 'react*']
   */
  packageNames?: string[]
  /**
   * version map<pkgName, version>
   *  prefer use this as version map if provided
   *  pkgName can be a pattern like @babel/*
   *  if packageNames also provided, will fetch missing versions
   * @example
   * {'@babel/*': '7.0.0', 'parcel': '^2.0.0', '@types/react': '~18.0.0'}
   */
  versionMap?: IVersionMap
  /**
   * npm version strategy
   *  default to 'max-stable'
   */
  versionPickStrategy?: IVersionPickStrategy
  /**
   * version range strategy, use retain by default
   */
  versionRangeStrategy?: IVersionRangeStrategy
  /** only check, with package.json files untouched */
  checkOnly?: boolean
  /**
   * update version to the exact given version
   *  set to false only update when existing version range is not satisfied
   * @default true
   */
  exact?: boolean
}

Tips: you may need to reinstall your workspaces dependence if anything changed

fixpack

Make all your package.json files are written in same criterion: sorting fields, validating required fields. This feature is powered by fixpack.

also available as command fixpack

e.g.

import { fixpack } from 'lerna-ci'

const updatedPkgs = await fixpack()
// [{name: 'my-package', version: '1.0.1', private: false, location: '/Users/xx/work/monorepo/my-package'}]

Type Declarations:

fixpack (options?: IFixPackOptions) => Promise<IPackageDigest[]>

export interface IFixPackOptions {
  /**
   * which package's should be fixed
   */
  packageFilter?: IPackageFilterOptions
  /**
   * package fix configuration
   *  check source <src/fixpack-all/config.ts> for default configuration
   *  see https://github.com/HenrikJoreteg/fixpack#configuration for details
   */
  config?: any
}

getChanged

List all changed packages since last release, it requires package lerna or @changesets/cli to be installed.

also available as command changed

e.g.

import { getChanged } from 'lerna-ci'

const changedPkgs = await getChanged()
// [{name: 'my-package', version: '1.0.1', private: false, location: '/Users/xx/work/monorepo/my-package'}]

Type Declarations:

getChanged() => Promise<IPackageDigest[]>

getRepoNpmClient

get current monorepo preferred npm client

e.g.

import { getRepoNpmClient } from 'lerna-ci'

// will return npm for default if not specified
//   get `yarn-next` when yarn version >= 2.0 found 
getRepoNpmClient().then(client => console.log(client))
// yarn

getVersionFormRegistry

get version from npm registry, you can get the latest version or the max version

e.g.

import { getVersionFormRegistry } from 'lerna-ci'

getVersionFormRegistry(options: IGetPkgVersionFromRegistryOptions) => Promise<string | undefined>

export interface IGetPkgVersionFromRegistryOptions {
  /** package name */
  pkgName: string
  /** strategy: latest or max */
  versionStrategy?: IVersionPickStrategy
  /**
   * specified version, to check for existence
   *  return itself if found, otherwise return empty string
   */
  version?: string
  /**
   * preferred npm client, auto detect if omitted
   */
  npmClient?: 'yarn' | 'yarn-next' | 'npm' | 'pnpm'
}

tips: if you want to fetch version from a npm mirror or custom registry, you should specify the mirror in the .yarnrc or .npmrc file

getVersionsFromRegistry

batch version of getVersionFormRegistry, but return an object(key is package name, value is version)

Type Declarations:

getVersionsFromRegistry(options: IGetPkgVersionsFromRegistryOptions) => Promise<string | undefined>

export interface IGetPkgVersionsFromRegistryOptions {
  /**
   * package names
   */
  pkgNames: string[]
  /**
   * version pick strategy
   *  max: max package version
   *  max-stable: max stable package version
   *  latest: latest release package version
   * @default max
   */
  versionStrategy?: 'max' | 'latest' | 'max-stable'
  /**
   * preferred npm client, detect automatically if not provided
   */
  npmClient?: 'yarn' | 'yarn-next' | 'npm' | 'pnpm'
}

getPackageVersionsFromGit

get monorepo package version map from git tag list(only tags in packageName@versionNumber like react@1.0.0 will be recognized).
caution: this api will run git fetch origin --prune --tags to sync tags from server, local un-pushed tags will be removed

e.g.

import { getPackageVersionsFromGit } from 'lerna-ci'

// will return npm for default if not specified
getPackageVersionsFromGit().then(ver => console.log(ver))
// {'duplex-message': '1.1.2', 'simple-electron-ipc': '1.1.2'}

Type Declarations:

getPackageVersionsFromGit(type: 'latest' | 'max' = 'latest') => Promise<Record<string, string>>

isLernaAvailable

check whether lerna is installed in current repo

e.g.

import { isLernaAvailable } from 'lerna-ci'

isLernaAvailable().then(isInstalled => console.log(isInstalled))
// true

getGitRoot

get git root path of current repo

e.g.

import { getGitRoot } from 'lerna-ci'

// return false if not in a git repo
const maxVer = await getGitRoot()
// /User/xx/work/monorepo

getProjectRoot

get current project's root path (which contains a package.json file)

e.g.

import { getProjectRoot } from 'lerna-ci'

// throw error if not in a valid frontend project
const maxVer = await getProjectRoot()
// /User/xx/work/monorepo

maxVersion

get max version(compare in semver) from a version list

e.g.

import { maxVersion } from 'lerna-ci'

const maxVer = maxVersion('0.1', '0.0.1', '1.0.0-alpha.1', '1.0.0')
// 1.0.0

pickOne

pick a value from a list with a custom compare method

e.g.

import { pickOne } from 'lerna-ci'

const picked = pickOne([{name: 'Lisa', age: 10}, {name: 'Janie', age: 12}, {name: 'Marry', age: 9}], (a, b) => a.age - b.age)
// {name: 'Janie', age: 12}

Type Declarations:

pickOne<V>(list: V[], compare: ICompare<V>) => V | undefined

// return `a` if result >= 0, or return `b`
type ICompare<V> = ((a: V, b: V) => -1 | 0 | 1

Configuration file

You may also add config for these commands via following ways(powered by cosmiconfig) so that you don't need to specify the arguments:

  • add lerna-ci field to package.json in the root of the project
  • add .lerna-circ file in the root of the project with json or yaml format
  • add .lerna-circ.json, .lerna-circ.yaml, .lerna-circ.yml or .lerna-circ.cjs file in the root of the project
  • add lerna-ci.config.js or lerna-ci.config.cjs file in the root of the project

all these configurations should return an object with the following properties:

Breaking changes

If you are updating from 0.0.x, you should be careful about following changes:

  1. default configuration for fixpack has been changed, you may restore the former behavior by setting fixpack.config to old configuration in configuration file
  2. if you are using APIs, most useful APIs are renamed for better understanding, but no feature is removed, you may read docs above to upgrade