lerna-update-wizard
Advanced tools
Comparing version 0.10.0 to 0.11.0
@@ -6,3 +6,3 @@ { | ||
}, | ||
"version": "0.10.0", | ||
"version": "0.11.0", | ||
"main": "index.js", | ||
@@ -9,0 +9,0 @@ "license": "MIT", |
@@ -73,2 +73,24 @@ # Lerna Update Wizard | ||
### Non-interactive mode | ||
The script can run without prompting you for input. Simply specify the `--non-interactive` flag: | ||
```bash | ||
$ lernaupdate --non-interactive --dependency lodash@4.2.1 ./my-project | ||
``` | ||
The script will tell you if you need to specify any additional input flags based on the state of your mono repo. | ||
For instance, you might need/wish to include information about which packages to affect and which type of installation to perform if the dependency is a first-time install: | ||
```bash | ||
$ lernaupdate --non-interactive \ | ||
--dependency lodash@4.2.1 \ | ||
--packages packages/utils,packages/tools \ | ||
--new-installs-mode dev \ | ||
./my-project | ||
``` | ||
**Note: Git features are not available for `--non-interactive` mode.** | ||
### Yarn support | ||
@@ -75,0 +97,0 @@ |
417
src/index.js
@@ -14,2 +14,4 @@ const path = require("path"); | ||
const plural = require("./utils/plural"); | ||
const invariant = require("./utils/invariant"); | ||
const parseDependency = require("./utils/parseDependency"); | ||
const sanitizeGitBranchName = require("./utils/sanitizeGitBranchName"); | ||
@@ -28,7 +30,15 @@ | ||
// Validate flags | ||
flags.nonInteractive && | ||
invariant( | ||
flags.dependency, | ||
"`--dependency` option must be specified in non-interactive mode" | ||
); | ||
const projectPackageJsonPath = resolve(projectDir, "package.json"); | ||
if (!(await fileExists(projectPackageJsonPath))) { | ||
throw new Error("No 'package.json' found in specified directory"); | ||
} | ||
invariant( | ||
await fileExists(projectPackageJsonPath), | ||
"No 'package.json' found in specified directory" | ||
); | ||
@@ -51,6 +61,8 @@ const { name: projectName } = require(projectPackageJsonPath); | ||
const defaultPackagesGlobs = flags.packages | ||
? flags.packages.split(",") | ||
: lernaConfig.packages || ["packages/*"]; | ||
const packagesRead = await globby( | ||
(lernaConfig.packages || ["packages/*"]).map(glob => | ||
resolve(projectDir, glob, "package.json") | ||
), | ||
defaultPackagesGlobs.map(glob => resolve(projectDir, glob, "package.json")), | ||
{ expandDirectories: true } | ||
@@ -67,5 +79,3 @@ ); | ||
if (packages.length === 0) { | ||
throw new Error("No packages found. Is this a Lerna project?"); | ||
} | ||
invariant(packages.length > 0, "No packages found. Is this a Lerna project?"); | ||
@@ -147,50 +157,57 @@ ui.logBottom(""); | ||
const { targetDependency } = await inquirer.prompt([ | ||
{ | ||
type: "autocomplete", | ||
name: "targetDependency", | ||
message: "Select a dependency to upgrade:", | ||
pageSize: 15, | ||
source: (_ignore_, input) => { | ||
const itemize = value => ({ | ||
value, | ||
name: `${chalk.white(value)} ${chalk[dependencyMap[value].color]( | ||
`(${plural( | ||
"version", | ||
"versions", | ||
dependencyMap[value].versions.length | ||
)})` | ||
)}`, | ||
}); | ||
let targetDependency = | ||
flags.dependency && parseDependency(flags.dependency).name; | ||
const sorter = flags.dedupe | ||
? (a, b) => | ||
dependencyMap[b].versions.length - | ||
dependencyMap[a].versions.length | ||
: undefined; | ||
if (!targetDependency) { | ||
const { targetDependency: promptedTarget } = await inquirer.prompt([ | ||
{ | ||
type: "autocomplete", | ||
name: "targetDependency", | ||
message: "Select a dependency to upgrade:", | ||
pageSize: 15, | ||
source: (_ignore_, input) => { | ||
const itemize = value => ({ | ||
value, | ||
name: `${chalk.white(value)} ${chalk[dependencyMap[value].color]( | ||
`(${plural( | ||
"version", | ||
"versions", | ||
dependencyMap[value].versions.length | ||
)})` | ||
)}`, | ||
}); | ||
let results = input | ||
? allDependencies | ||
.filter(name => new RegExp(input).test(name)) | ||
.sort(sorter) | ||
.map(itemize) | ||
: allDependencies.sort(sorter).map(itemize); | ||
const sorter = flags.dedupe | ||
? (a, b) => | ||
dependencyMap[b].versions.length - | ||
dependencyMap[a].versions.length | ||
: undefined; | ||
if (input && !allDependencies.includes(input)) { | ||
results = [ | ||
...results, | ||
{ | ||
name: `${input} ${chalk.green.bold("[+ADD]")}`, | ||
value: input, | ||
}, | ||
]; | ||
} | ||
let results = input | ||
? allDependencies | ||
.filter(name => new RegExp(input).test(name)) | ||
.sort(sorter) | ||
.map(itemize) | ||
: allDependencies.sort(sorter).map(itemize); | ||
return Promise.resolve(results); | ||
if (input && !allDependencies.includes(input)) { | ||
results = [ | ||
...results, | ||
{ | ||
name: `${input} ${chalk.green.bold("[+ADD]")}`, | ||
value: input, | ||
}, | ||
]; | ||
} | ||
return Promise.resolve(results); | ||
}, | ||
}, | ||
}, | ||
]); | ||
]); | ||
const isNewDependency = !allDependencies.includes(targetDependency); | ||
targetDependency = promptedTarget; | ||
} | ||
// Look up NPM dependency and its versions | ||
const npmPackageInfoRaw = await runCommand( | ||
@@ -210,65 +227,113 @@ `npm info ${targetDependency} versions dist-tags --json`, | ||
const { targetPackages } = await inquirer.prompt([ | ||
{ | ||
type: "checkbox", | ||
name: "targetPackages", | ||
message: "Select packages to affect:", | ||
pageSize: 15, | ||
choices: packages.map(({ config: { name: packageName } }) => { | ||
if (isNewDependency) { | ||
// Target packages selection | ||
const isNewDependency = !allDependencies.includes(targetDependency); | ||
if (flags.nonInteractive && isNewDependency) { | ||
invariant( | ||
flags.newInstallsMode, | ||
`"${targetDependency}" is a first-time install for one or more packages.`, | ||
"In non-interactive mode you must specify the --new-installs-mode flag (prod|dev|peer) in this situation." | ||
); | ||
} | ||
let targetPackages; | ||
if (flags.nonInteractive && !flags.packages) { | ||
const installedPackages = packages | ||
.filter( | ||
({ config: { name } }) => | ||
!!( | ||
dependencyMap[targetDependency] && | ||
dependencyMap[targetDependency].packs[name] | ||
) | ||
) | ||
.map(({ config: { name } }) => name); | ||
invariant( | ||
installedPackages.length > 0, | ||
`No packages contain the dependency "${targetDependency}".`, | ||
"In non-interactive mode you must specify the --packages flag in this situation,", | ||
"so the script can know which packages install it in." | ||
); | ||
targetPackages = installedPackages; | ||
} else if (flags.packages) { | ||
targetPackages = packages.map(({ config: { name } }) => name); | ||
} | ||
if (!targetPackages) { | ||
const { targetPackages: promptedTarget } = await inquirer.prompt([ | ||
{ | ||
type: "checkbox", | ||
name: "targetPackages", | ||
message: "Select packages to affect:", | ||
pageSize: 15, | ||
choices: packages.map(({ config: { name: packageName } }) => { | ||
if (isNewDependency) { | ||
return { | ||
name: packageName, | ||
value: packageName, | ||
checked: false, | ||
}; | ||
} | ||
const { version, source } = | ||
dependencyMap[targetDependency].packs[packageName] || {}; | ||
const versionBit = version ? ` (${version})` : ""; | ||
const sourceBit = | ||
source === "devDependencies" ? chalk.white(" (dev)") : ""; | ||
return { | ||
name: packageName, | ||
name: `${packageName}${versionBit}${sourceBit}`, | ||
value: packageName, | ||
checked: false, | ||
checked: !!version, | ||
}; | ||
} | ||
}), | ||
}, | ||
]); | ||
const { version, source } = | ||
dependencyMap[targetDependency].packs[packageName] || {}; | ||
targetPackages = promptedTarget; | ||
} | ||
const versionBit = version ? ` (${version})` : ""; | ||
const sourceBit = | ||
source === "devDependencies" ? chalk.white(" (dev)") : ""; | ||
// Target version selection | ||
let targetVersion = | ||
flags.dependency && parseDependency(flags.dependency).version; | ||
return { | ||
name: `${packageName}${versionBit}${sourceBit}`, | ||
value: packageName, | ||
checked: !!version, | ||
}; | ||
}), | ||
}, | ||
]); | ||
if (!targetVersion) { | ||
const npmVersions = npmPackageInfo.versions.reverse(); | ||
const npmDistTags = npmPackageInfo["dist-tags"]; | ||
const npmVersions = npmPackageInfo.versions.reverse(); | ||
const npmDistTags = npmPackageInfo["dist-tags"]; | ||
const highestInstalled = | ||
!isNewDependency && | ||
dependencyMap[targetDependency].versions.sort(semverCompare).pop(); | ||
const highestInstalled = | ||
!isNewDependency && | ||
dependencyMap[targetDependency].versions.sort(semverCompare).pop(); | ||
const availableVersions = [ | ||
...Object.entries(npmDistTags).map(([tag, version]) => ({ | ||
name: `${version} ${chalk.bold(`#${tag}`)}`, | ||
value: version, | ||
})), | ||
!isNewDependency && { | ||
name: `${highestInstalled} ${chalk.bold("Highest installed")}`, | ||
value: highestInstalled, | ||
}, | ||
...npmVersions.filter( | ||
version => | ||
version !== highestInstalled && | ||
!Object.values(npmDistTags).includes(version) | ||
), | ||
].filter(Boolean); | ||
const availableVersions = [ | ||
...Object.entries(npmDistTags).map(([tag, version]) => ({ | ||
name: `${version} ${chalk.bold(`#${tag}`)}`, | ||
value: version, | ||
})), | ||
!isNewDependency && { | ||
name: `${highestInstalled} ${chalk.bold("Highest installed")}`, | ||
value: highestInstalled, | ||
}, | ||
...npmVersions.filter( | ||
version => | ||
version !== highestInstalled && | ||
!Object.values(npmDistTags).includes(version) | ||
), | ||
].filter(Boolean); | ||
const { targetVersion: promptedTarget } = await inquirer.prompt([ | ||
{ | ||
type: "semverList", | ||
name: "targetVersion", | ||
message: "Select version to install:", | ||
pageSize: 10, | ||
choices: availableVersions, | ||
}, | ||
]); | ||
const { targetVersion } = await inquirer.prompt([ | ||
{ | ||
type: "semverList", | ||
name: "targetVersion", | ||
message: "Select version to install:", | ||
pageSize: 10, | ||
choices: availableVersions, | ||
}, | ||
]); | ||
targetVersion = promptedTarget; | ||
} | ||
@@ -303,3 +368,3 @@ perf.start(); | ||
} | ||
} else { | ||
} else if (!flags.newInstallsMode) { | ||
const { targetSource } = await inquirer.prompt([ | ||
@@ -320,2 +385,8 @@ { | ||
source = targetSource; | ||
} else { | ||
source = { | ||
prod: "dependencies", | ||
dev: "devDependencies", | ||
peer: "peerDependencies", | ||
}[flags.newInstallsMode]; | ||
} | ||
@@ -358,74 +429,82 @@ | ||
const userName = ( | ||
(await runCommand("git config --get github.user", { logOutput: false })) || | ||
(await runCommand("whoami", { logOutput: false })) || | ||
"upgrade" | ||
) | ||
.split("\n") | ||
.shift(); | ||
if (!flags.nonInteractive) { | ||
const userName = ( | ||
(await runCommand("git config --get github.user", { | ||
logOutput: false, | ||
})) || | ||
(await runCommand("whoami", { logOutput: false })) || | ||
"upgrade" | ||
) | ||
.split("\n") | ||
.shift(); | ||
const { | ||
shouldCreateGitBranch, | ||
shouldCreateGitCommit, | ||
gitBranchName, | ||
gitCommitMessage, | ||
} = await inquirer.prompt([ | ||
{ | ||
type: "confirm", | ||
name: "shouldCreateGitBranch", | ||
message: "Do you want to create a new git branch for the change?", | ||
}, | ||
{ | ||
type: "input", | ||
name: "gitBranchName", | ||
message: "Enter a name for your branch:", | ||
when: ({ shouldCreateGitBranch }) => shouldCreateGitBranch, | ||
default: sanitizeGitBranchName( | ||
`${userName}/${targetDependency}-${targetVersion}` | ||
), | ||
}, | ||
{ | ||
type: "confirm", | ||
name: "shouldCreateGitCommit", | ||
message: "Do you want to create a new git commit for the change?", | ||
}, | ||
{ | ||
type: "input", | ||
name: "gitCommitMessage", | ||
message: "Enter a git commit message:", | ||
when: ({ shouldCreateGitCommit }) => shouldCreateGitCommit, | ||
default: `Upgrade dependency: ${targetDependency}@${targetVersion}`, | ||
}, | ||
]); | ||
const { | ||
shouldCreateGitBranch, | ||
shouldCreateGitCommit, | ||
gitBranchName, | ||
gitCommitMessage, | ||
} = await inquirer.prompt([ | ||
{ | ||
type: "confirm", | ||
name: "shouldCreateGitBranch", | ||
message: "Do you want to create a new git branch for the change?", | ||
}, | ||
{ | ||
type: "input", | ||
name: "gitBranchName", | ||
message: "Enter a name for your branch:", | ||
when: ({ shouldCreateGitBranch }) => shouldCreateGitBranch, | ||
default: sanitizeGitBranchName( | ||
`${userName}/${targetDependency}-${targetVersion}` | ||
), | ||
}, | ||
{ | ||
type: "confirm", | ||
name: "shouldCreateGitCommit", | ||
message: "Do you want to create a new git commit for the change?", | ||
}, | ||
{ | ||
type: "input", | ||
name: "gitCommitMessage", | ||
message: "Enter a git commit message:", | ||
when: ({ shouldCreateGitCommit }) => shouldCreateGitCommit, | ||
default: `Upgrade dependency: ${targetDependency}@${targetVersion}`, | ||
}, | ||
]); | ||
if (shouldCreateGitBranch) { | ||
const createCmd = `git checkout -b ${gitBranchName}`; | ||
await runCommand(`cd ${projectDir} && ${createCmd}`, { | ||
startMessage: `${chalk.white.bold(projectName)}: ${createCmd}`, | ||
endMessage: chalk.green(`Branch created ✓`), | ||
}); | ||
} | ||
if (shouldCreateGitBranch) { | ||
const createCmd = `git checkout -b ${gitBranchName}`; | ||
await runCommand(`cd ${projectDir} && ${createCmd}`, { | ||
startMessage: `${chalk.white.bold(projectName)}: ${createCmd}`, | ||
endMessage: chalk.green(`Branch created ✓`), | ||
}); | ||
} | ||
if (shouldCreateGitCommit) { | ||
const subMessage = targetPackages | ||
.reduce((prev, depName) => { | ||
const fromVersion = | ||
!isNewDependency && | ||
dependencyMap[targetDependency].packs[depName].version; | ||
if (shouldCreateGitCommit) { | ||
const subMessage = targetPackages | ||
.reduce((prev, depName) => { | ||
const fromVersion = | ||
!isNewDependency && | ||
dependencyMap[targetDependency].packs[depName].version; | ||
if (fromVersion === targetVersion) return prev; | ||
if (fromVersion === targetVersion) return prev; | ||
return fromVersion | ||
? [...prev, `* ${depName}: ${fromVersion} → ${targetVersion}`] | ||
: [...prev, `* ${depName}: ${targetVersion}`]; | ||
}, []) | ||
.join("\n"); | ||
return fromVersion | ||
? [...prev, `* ${depName}: ${fromVersion} → ${targetVersion}`] | ||
: [...prev, `* ${depName}: ${targetVersion}`]; | ||
}, []) | ||
.join("\n"); | ||
const createCmd = `git add . && git commit -m '${gitCommitMessage}' -m '${subMessage}'`; | ||
await runCommand(`cd ${projectDir} && ${createCmd}`, { | ||
startMessage: `${chalk.white.bold(projectName)}: git add . && git commit`, | ||
endMessage: chalk.green(`Commit created ✓`), | ||
logOutput: false, | ||
}); | ||
const createCmd = `git add . && git commit -m '${gitCommitMessage}' -m '${subMessage}'`; | ||
await runCommand(`cd ${projectDir} && ${createCmd}`, { | ||
startMessage: `${chalk.white.bold( | ||
projectName | ||
)}: git add . && git commit`, | ||
endMessage: chalk.green(`Commit created ✓`), | ||
logOutput: false, | ||
}); | ||
} | ||
} else { | ||
process.exit(); | ||
} | ||
}; |
@@ -94,2 +94,59 @@ const { default: runProgram } = require("./utils/runProgram"); | ||
}); | ||
describe("non-interactive", () => { | ||
it("Adds the dependency via the --dependency flag", async () => { | ||
// eslint-disable-next-line | ||
jest.setTimeout(100000); | ||
const projectPath = await generateProject({ | ||
name: "project-add-dependency-non-interactive", | ||
packages: [ | ||
{ name: "sub-package-a" }, | ||
{ | ||
name: "sub-package-b", | ||
dependencies: { treediff: "0.1.0" }, | ||
}, | ||
{ name: "sub-package-c" }, | ||
{ name: "sub-package-d", dependencies: { lodash: "0.2.0" } }, | ||
], | ||
}); | ||
await runProgram( | ||
projectPath, | ||
` | ||
❯◯ sub-package-a | ||
◯ sub-package-b | ||
◯ sub-package-c | ||
◯ sub-package-d | ||
>>> input SPACE | ||
❯◉ sub-package-a | ||
>>> input ENTER | ||
Select dependency installation type for "sub-package-a" | ||
>>> input ENTER | ||
? Do you want to create a new git branch for the change? (Y/n) | ||
>>> input CTRL+C | ||
`, | ||
{ | ||
flags: "--dependency promise-react-component@0.0.2", | ||
} | ||
); | ||
expect( | ||
await fileExists( | ||
resolve( | ||
projectPath, | ||
"packages/sub-package-a/node_modules/promise-react-component/package.json" | ||
) | ||
), | ||
"to be true" | ||
); | ||
}); | ||
}); | ||
}); |
@@ -50,2 +50,3 @@ const fs = require("fs-extra"); | ||
...pOptions, | ||
git: false, | ||
prefixPath: path.resolve(p, pOptions.moduleDirName || "packages"), | ||
@@ -57,2 +58,8 @@ }, | ||
} | ||
if (options.git) { | ||
await exec( | ||
`cd ${p} && echo "node_modules" > .gitignore && git init && git add . && git commit -nam 'initial commit'` | ||
); | ||
} | ||
}; | ||
@@ -59,0 +66,0 @@ |
@@ -102,3 +102,3 @@ const chalk = require("chalk"); | ||
inputSequence, | ||
{ log = !!process.env.CI || !!process.env.DEBUG } = {} | ||
{ log = !!process.env.CI || !!process.env.DEBUG, flags = "" } = {} | ||
) => { | ||
@@ -114,3 +114,3 @@ const program = | ||
const cmd = `./bin/lernaupdate ${projectPath}`; | ||
const cmd = `./bin/lernaupdate${flags && ` ${flags}`} ${projectPath}`; | ||
const proc = spawn(cmd, { shell: true }); | ||
@@ -117,0 +117,0 @@ |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
22910629
39
1813
112
13
4