├── .nvmrc
├── .npmrc
├── tasks
├── dependencies
│ ├── test
│ │ ├── inquire.spec.js
│ │ ├── extraneous.spec.js
│ │ ├── extraneous.it.js
│ │ ├── unmanaged.it.js
│ │ ├── sync.it.js
│ │ └── latest.it.js
│ ├── index.js
│ ├── lib
│ │ ├── inquire.js
│ │ ├── sync.js
│ │ ├── extraneous.js
│ │ ├── unmanaged.js
│ │ └── latest.js
│ ├── package.json
│ └── README.md
├── idea
│ ├── files
│ │ ├── vcs.xml
│ │ ├── root_module.iml.tmpl
│ │ ├── modules.xml.tmpl
│ │ ├── module.iml.tmpl
│ │ └── workspace.xml.tmpl
│ ├── package.json
│ ├── lib
│ │ └── templates.js
│ ├── README.md
│ ├── index.js
│ └── test
│ │ └── idea.spec.js
├── depcheck
│ ├── README.md
│ ├── package.json
│ ├── index.js
│ └── test
│ │ └── depcheck.spec.js
├── npmfix
│ ├── README.md
│ ├── package.json
│ ├── index.js
│ └── test
│ │ └── npmfix.spec.js
└── modules
│ ├── package.json
│ ├── README.md
│ ├── index.js
│ └── test
│ └── modules.spec.js
├── renovate.json
├── .travis.yml
├── .gitignore
├── test-utils
├── README.md
├── test
│ └── module-builder.spec.js
├── package.json
├── index.js
└── lib
│ └── module-builder.js
├── .prettierrc
├── lerna-script
├── lib
│ ├── task-runner.js
│ ├── fs.js
│ ├── packages.js
│ ├── exec.js
│ ├── detect-changes.js
│ ├── iterators.js
│ └── filters.js
├── test
│ ├── utils.js
│ ├── packages.spec.js
│ ├── task-runner.spec.js
│ ├── fs.spec.js
│ ├── cli.spec.js
│ ├── exec.spec.js
│ ├── iterators.spec.js
│ ├── detect-changes.spec.js
│ └── filters.spec.js
├── index.js
├── package.json
├── bin
│ └── cli.js
└── README.md
├── package.json
├── LICENSE
├── lerna.json
├── lerna.js
└── README.md
/.nvmrc:
--------------------------------------------------------------------------------
1 | 8
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/tasks/dependencies/test/inquire.spec.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["config:base"],
3 | "automerge": true
4 | }
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | branches:
3 | only:
4 | - master
5 | script:
6 | - npm run test
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | *.iml
3 | node_modules/
4 | *.log
5 | target/
6 | .DS_Store
7 | .lerna
8 | package-lock.json
9 | .tern-port
10 |
--------------------------------------------------------------------------------
/test-utils/README.md:
--------------------------------------------------------------------------------
1 | # lerna-script-test-utils
2 |
3 | # install
4 |
5 | ```bash
6 | npm install --save-dev lerna-script-test-utils
7 | ```
8 |
9 | # Usage
10 |
11 | TBD
12 |
--------------------------------------------------------------------------------
/test-utils/test/module-builder.spec.js:
--------------------------------------------------------------------------------
1 | const {expect} = require('chai')
2 |
3 | describe('module builder', function () {
4 | it('should pass', () => {
5 | expect(true).to.equal(true)
6 | })
7 | })
8 |
--------------------------------------------------------------------------------
/tasks/idea/files/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "none",
3 | "arrowParens": "avoid",
4 | "printWidth": 100,
5 | "semi": false,
6 | "singleQuote": true,
7 | "bracketSpacing": false,
8 | "overrides": [
9 | {
10 | "files": "*.md",
11 | "options": {
12 | "semi": true
13 | }
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/tasks/idea/files/root_module.iml.tmpl:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/tasks/dependencies/index.js:
--------------------------------------------------------------------------------
1 | const extraneous = require('./lib/extraneous'),
2 | sync = require('./lib/sync'),
3 | unmanaged = require('./lib/unmanaged'),
4 | latest = require('./lib/latest')
5 |
6 | module.exports.sync = sync.task
7 | module.exports.unmanaged = unmanaged.task
8 | module.exports.extraneous = extraneous.task
9 | module.exports.latest = latest.task
10 |
--------------------------------------------------------------------------------
/tasks/dependencies/test/extraneous.spec.js:
--------------------------------------------------------------------------------
1 | const {logExtraneous} = require('../lib/extraneous'),
2 | {loggerMock} = require('lerna-script-test-utils'),
3 | {expect} = require('chai').use(require('sinon-chai'))
4 |
5 | describe('extraneous', () => {
6 | describe('logExtraneous', () => {
7 | it('should not log extraneous if none present', () => {
8 | const log = loggerMock()
9 | logExtraneous({deps: {}}, log, 'deps')
10 |
11 | expect(log.error).to.not.have.been.called
12 | })
13 | })
14 | })
15 |
--------------------------------------------------------------------------------
/tasks/idea/files/modules.xml.tmpl:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{#if options.addRoot}}
6 |
7 | {{/if}}
8 | {{#each modules}}
9 |
10 | {{/each}}
11 |
12 |
13 |
--------------------------------------------------------------------------------
/tasks/dependencies/lib/inquire.js:
--------------------------------------------------------------------------------
1 | const inquirer = require('inquirer')
2 |
3 | module.exports = ({message, choiceGroups}) => {
4 | const inquirerChoices = []
5 |
6 | choiceGroups.forEach(({name, choices}) => {
7 | inquirerChoices.push(new inquirer.Separator(name))
8 | choices.forEach(c => inquirerChoices.push(c))
9 | })
10 |
11 | return inquirer
12 | .prompt([
13 | {
14 | type: 'checkbox',
15 | message,
16 | name: 'boo',
17 | pageSize: 20,
18 | choices: inquirerChoices
19 | }
20 | ])
21 | .then(answers => answers.boo)
22 | }
23 |
--------------------------------------------------------------------------------
/tasks/depcheck/README.md:
--------------------------------------------------------------------------------
1 | # lerna-script-tasks-depcheck
2 |
3 | [lerna-script](../..) task that:
4 |
5 | - runs [depcheck](https://github.com/depcheck/depcheck) for all modules.
6 |
7 | ## install
8 |
9 | ```bash
10 | npm install --save-dev lerna-script-tasks-depcheck
11 | ```
12 |
13 | ## API
14 |
15 | ### ({[packages], [depcheck]})(log): Promise
16 |
17 | Run depcheck for all modules incrementally.
18 |
19 | Parameters:
20 |
21 | - packages - list of packages to to run depcheck for or defaults to ones defined in `lerna.json`;
22 | - depcheck - options for [depcheck](https://github.com/depcheck/depcheck) task.
23 | - log - `npmlog` instance.
24 |
--------------------------------------------------------------------------------
/tasks/npmfix/README.md:
--------------------------------------------------------------------------------
1 | # lerna-script-tasks-npmfix
2 |
3 | [lerna-script](../..) task that:
4 |
5 | - updates 'homepage' `package.json` value to location in repo for existing github `origin` remote;
6 | - updates 'repository' `package.json` value to location in repo for existing github `origin` remote;
7 |
8 | ## install
9 |
10 | ```bash
11 | npm install --save-dev lerna-script-tasks-npmfix
12 | ```
13 |
14 | ## API
15 |
16 | ### ({[packages]})(log): Promise
17 |
18 | Updates `homepage`, `repository` urls for `packages`.
19 |
20 | Parameters:
21 |
22 | - packages - list of packages to generate idea project for or defaults to ones defined in `lerna.json`;
23 | - log - `npmlog` instance.
24 |
--------------------------------------------------------------------------------
/tasks/idea/files/module.iml.tmpl:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{#each config.sourceFolders}}
7 |
8 | {{/each}}
9 | {{#each config.excludeFolders}}
10 |
11 | {{/each}}
12 | {{#each config.excludePatterns}}
13 |
14 | {{/each}}
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/lerna-script/lib/task-runner.js:
--------------------------------------------------------------------------------
1 | module.exports = ({process, log}) => (tasks, task) => {
2 | if (!isTaskProvided(task)) {
3 | log.info('lerna-script', 'No task provided.', getAvailableTaskRunners(tasks))
4 | process.exit(0)
5 | } else if (!isTaskPresent(tasks, task)) {
6 | log.error('lerna-script', `Unable to find task "${task}"`)
7 | log.error('lerna-script', getAvailableTaskRunners(tasks))
8 | process.exit(1)
9 | } else {
10 | log.info('lerna-script', `executing task: "${task}"`)
11 | return Promise.resolve().then(() => tasks[task](log))
12 | }
13 | }
14 |
15 | function isTaskProvided(task) {
16 | return !!task
17 | }
18 |
19 | function isTaskPresent(tasks, task) {
20 | return tasks[task]
21 | }
22 |
23 | function getAvailableTaskRunners(tasks) {
24 | return `Available tasks: "${Object.keys(tasks).join('", "')}"`
25 | }
26 |
--------------------------------------------------------------------------------
/lerna-script/lib/fs.js:
--------------------------------------------------------------------------------
1 | const {EOL} = require('os'),
2 | Promise = require('bluebird'),
3 | fs = Promise.promisifyAll(require('fs')),
4 | {join} = require('path')
5 |
6 | function readFile(lernaPackage) {
7 | return (relativePath, convert = content => content.toString()) => {
8 | return fs.readFileAsync(join(lernaPackage.location, relativePath)).then(convert)
9 | }
10 | }
11 |
12 | function writeFile(lernaPackage) {
13 | return (relativePath, content, converter) => {
14 | let toWrite = content
15 | if (converter) {
16 | toWrite = converter(content)
17 | } else if (content === Object(content)) {
18 | toWrite = JSON.stringify(content, null, 2) + EOL
19 | }
20 | return fs.writeFileAsync(join(lernaPackage.location, relativePath), toWrite)
21 | }
22 | }
23 |
24 | module.exports = {
25 | readFile,
26 | writeFile
27 | }
28 |
--------------------------------------------------------------------------------
/lerna-script/lib/packages.js:
--------------------------------------------------------------------------------
1 | const {getPackages} = require('@lerna/project'),
2 | batchPackages = require('@lerna/batch-packages'),
3 | Package = require('@lerna/package'),
4 | _ = require('lodash'),
5 | {join} = require('path'),
6 | npmlog = require('npmlog')
7 |
8 | async function loadPackages({log = npmlog} = {log: npmlog}) {
9 | log.verbose('loadPackages', 'using default packageConfigs')
10 |
11 | const loadedPackages = await getPackages(process.cwd())
12 | const batched = batchPackages(loadedPackages, true)
13 | return _.flatten(batched)
14 | }
15 |
16 | async function loadRootPackage({log = npmlog} = {log: npmlog}) {
17 | const cwd = process.cwd()
18 | log.verbose('loadRootPackage', {cwd})
19 | return new Package(require(join(cwd, './package.json')), cwd)
20 | }
21 |
22 | module.exports = {
23 | loadPackages,
24 | loadRootPackage
25 | }
26 |
--------------------------------------------------------------------------------
/test-utils/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lerna-script-test-utils",
3 | "version": "1.3.2",
4 | "description": "test utils for lerna-script modules",
5 | "author": "vilius@wix.com",
6 | "license": "BSD",
7 | "homepage": "https://github.com/wix/lerna-script/tree/master/test-utils",
8 | "main": "index.js",
9 | "scripts": {
10 | "test": "mocha"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git@github.com:wix/lerna-script.git",
15 | "directory": "/test-utils"
16 | },
17 | "publishConfig": {
18 | "registry": "https://registry.npmjs.org"
19 | },
20 | "dependencies": {
21 | "bluebird": "3.7.2",
22 | "fs-extra": "8.1.0"
23 | },
24 | "peerDependencies": {
25 | "sinon": ">9.0.0"
26 | },
27 | "devDependencies": {
28 | "chai": "4.2.0",
29 | "mocha": "7.2.0",
30 | "sinon": "9.0.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/tasks/idea/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lerna-script-tasks-idea",
3 | "version": "1.4.0",
4 | "description": "task for start that generates intellij project for repo",
5 | "author": "vilius@wix.com",
6 | "license": "BSD",
7 | "homepage": "https://github.com/wix/lerna-script/tree/master/tasks/idea",
8 | "main": "index.js",
9 | "scripts": {
10 | "test": "mocha"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git@github.com:wix/lerna-script.git",
15 | "directory": "/tasks/idea"
16 | },
17 | "publishConfig": {
18 | "registry": "https://registry.npmjs.org"
19 | },
20 | "dependencies": {
21 | "handlebars": "4.7.6",
22 | "shelljs": "0.8.4"
23 | },
24 | "peerDependencies": {
25 | "lerna-script": ">=1.3.3"
26 | },
27 | "devDependencies": {
28 | "chai": "4.2.0",
29 | "lerna-script": "^1.4.0",
30 | "lerna-script-test-utils": "~1.3.2",
31 | "mocha": "7.2.0",
32 | "sinon": "9.0.2",
33 | "sinon-chai": "3.5.0"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/tasks/modules/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lerna-script-tasks-modules",
3 | "version": "1.4.0",
4 | "description": "module version sync task for lerna-script",
5 | "author": "vilius@wix.com",
6 | "license": "BSD",
7 | "homepage": "https://github.com/wix/lerna-script/tree/master/tasks/modules",
8 | "main": "index.js",
9 | "scripts": {
10 | "test": "mocha"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git@github.com:wix/lerna-script.git",
15 | "directory": "/tasks/modules"
16 | },
17 | "publishConfig": {
18 | "registry": "https://registry.npmjs.org"
19 | },
20 | "dependencies": {
21 | "deep-keys": "0.5.0",
22 | "lodash": "4.17.15"
23 | },
24 | "peerDependencies": {
25 | "lerna-script": ">=1.3.3"
26 | },
27 | "devDependencies": {
28 | "chai": "4.2.0",
29 | "lerna-script": "^1.4.0",
30 | "lerna-script-test-utils": "~1.3.2",
31 | "mocha": "7.2.0",
32 | "sinon": "9.0.2",
33 | "sinon-chai": "3.5.0"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lerna-script-modules",
3 | "private": true,
4 | "version": "1.0.0",
5 | "scripts": {
6 | "postinstall": "lerna bootstrap --no-ci",
7 | "clean": "lerna-script clean",
8 | "test": "lerna-script test",
9 | "ls": "lerna-script",
10 | "idea": "lerna-script idea",
11 | "release": "lerna publish",
12 | "deps:latest": "lerna-script deps:latest"
13 | },
14 | "devDependencies": {
15 | "husky": "4.2.5",
16 | "lerna": "3.21.0",
17 | "lerna-script": "1.3.0",
18 | "lerna-script-tasks-depcheck": "1.3.0",
19 | "lerna-script-tasks-dependencies": "1.3.0",
20 | "lerna-script-tasks-idea": "1.3.0",
21 | "lerna-script-tasks-modules": "1.3.0",
22 | "lerna-script-tasks-npmfix": "1.3.0",
23 | "lint-staged": "10.2.6",
24 | "prettier": "2.0.5"
25 | },
26 | "lint-staged": {
27 | "*": "prettier --write"
28 | },
29 | "husky": {
30 | "hooks": {
31 | "pre-commit": "lerna-script sync && lint-staged"
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tasks/npmfix/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lerna-script-tasks-npmfix",
3 | "version": "1.4.0",
4 | "description": "tasks for lerna-script that fixes links in package.json",
5 | "author": "Vilius Lukosius",
6 | "license": "ISC",
7 | "homepage": "https://github.com/wix/lerna-script/tree/master/tasks/npmfix",
8 | "main": "index.js",
9 | "scripts": {
10 | "test": "mocha"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git@github.com:wix/lerna-script.git",
15 | "directory": "/tasks/npmfix"
16 | },
17 | "publishConfig": {
18 | "registry": "https://registry.npmjs.org"
19 | },
20 | "dependencies": {
21 | "git-remote-url": "1.0.1",
22 | "hosted-git-info": "3.0.4"
23 | },
24 | "peerDependencies": {
25 | "lerna-script": ">=1.3.3"
26 | },
27 | "devDependencies": {
28 | "chai": "4.2.0",
29 | "lerna-script": "^1.4.0",
30 | "lerna-script-test-utils": "~1.3.2",
31 | "mocha": "7.2.0",
32 | "sinon": "9.0.2",
33 | "sinon-chai": "3.5.0"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/tasks/depcheck/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lerna-script-tasks-depcheck",
3 | "version": "1.4.0",
4 | "description": "tasks for running depcheck for all modules",
5 | "author": "Vilius Lukosius",
6 | "license": "ISC",
7 | "homepage": "https://github.com/wix/lerna-script/tree/master/tasks/depcheck",
8 | "main": "index.js",
9 | "scripts": {
10 | "test": "mocha"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git@github.com:wix/lerna-script.git",
15 | "directory": "/tasks/depcheck"
16 | },
17 | "publishConfig": {
18 | "registry": "https://registry.npmjs.org"
19 | },
20 | "dependencies": {
21 | "colors": "1.4.0",
22 | "depcheck": "0.9.2"
23 | },
24 | "peerDependencies": {
25 | "lerna-script": ">=1.3.3"
26 | },
27 | "devDependencies": {
28 | "chai": "4.2.0",
29 | "invert-promise": "^1.0.1",
30 | "lerna-script": "^1.4.0",
31 | "lerna-script-test-utils": "~1.3.2",
32 | "mocha": "7.2.0",
33 | "sinon": "9.0.2",
34 | "sinon-chai": "3.5.0"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/lerna-script/test/utils.js:
--------------------------------------------------------------------------------
1 | const index = require('..'),
2 | intercept = require('intercept-stdout')
3 |
4 | module.exports.asBuilt = async (project, {label, log} = {}) => {
5 | const resolved = await project
6 | return resolved.inDir(async ctx => {
7 | const lernaPackages = await index.loadPackages({log})
8 | lernaPackages.forEach(lernaPackage => index.changes.build(lernaPackage, {log})(label))
9 | ctx.exec('sleep 1') //so that second would rotate
10 | })
11 | }
12 |
13 | module.exports.asGitCommited = project => {
14 | return Promise.resolve(project).then(resolved =>
15 | resolved.inDir(ctx => {
16 | ctx.exec('git add -A && git commit -am "init"')
17 | })
18 | )
19 | }
20 |
21 | module.exports.captureOutput = () => {
22 | let capturedOutput = ''
23 | let detach
24 |
25 | beforeEach(
26 | () =>
27 | (detach = intercept(txt => {
28 | capturedOutput += txt
29 | }))
30 | )
31 |
32 | afterEach(() => {
33 | detach()
34 | capturedOutput = ''
35 | })
36 |
37 | return () => capturedOutput
38 | }
39 |
--------------------------------------------------------------------------------
/lerna-script/index.js:
--------------------------------------------------------------------------------
1 | const iterators = require('./lib/iterators'),
2 | packages = require('./lib/packages'),
3 | detectChanges = require('./lib/detect-changes'),
4 | filters = require('./lib/filters'),
5 | exec = require('./lib/exec'),
6 | fs = require('./lib/fs')
7 |
8 | module.exports.loadPackages = packages.loadPackages
9 | module.exports.loadRootPackage = packages.loadRootPackage
10 |
11 | module.exports.iter = {
12 | forEach: iterators.forEach,
13 | parallel: iterators.parallel,
14 | batched: iterators.batched
15 | }
16 |
17 | module.exports.changes = {
18 | build: detectChanges.markPackageBuilt,
19 | unbuild: detectChanges.markPackageUnbuilt,
20 | isBuilt: detectChanges.isPackageBuilt
21 | }
22 |
23 | module.exports.filters = {
24 | removeBuilt: filters.removeBuilt,
25 | gitSince: filters.removeGitSince,
26 | removeByGlob: filters.removeByGlob,
27 | includeFilteredDeps: filters.includeFilteredDeps
28 | }
29 |
30 | module.exports.exec = {
31 | command: exec.runCommand,
32 | script: exec.runScript
33 | }
34 |
35 | module.exports.fs = {
36 | readFile: fs.readFile,
37 | writeFile: fs.writeFile
38 | }
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Wix.com
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/tasks/dependencies/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lerna-script-tasks-dependencies",
3 | "version": "1.4.0",
4 | "description": "tasks for lerna-script that adds dependency management support",
5 | "author": "Vilius Lukosius",
6 | "license": "ISC",
7 | "homepage": "https://github.com/wix/lerna-script/tree/master/tasks/dependencies",
8 | "main": "index.js",
9 | "scripts": {
10 | "test": "mocha"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git@github.com:wix/lerna-script.git",
15 | "directory": "/tasks/dependencies"
16 | },
17 | "publishConfig": {
18 | "registry": "https://registry.npmjs.org"
19 | },
20 | "dependencies": {
21 | "deep-keys": "0.5.0",
22 | "inquirer": "^7.0.0",
23 | "lodash": "4.17.15",
24 | "ramda": "0.27.0",
25 | "semver": "7.3.2"
26 | },
27 | "peerDependencies": {
28 | "lerna-script": ">=1.3.3"
29 | },
30 | "devDependencies": {
31 | "bdd-stdin": "0.2.0",
32 | "chai": "4.2.0",
33 | "lerna-script": "^1.4.0",
34 | "lerna-script-test-utils": "~1.3.2",
35 | "mocha": "7.2.0",
36 | "sinon": "9.0.2",
37 | "sinon-chai": "3.5.0"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/tasks/idea/lib/templates.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs'),
2 | handlebars = require('handlebars').create()
3 |
4 | require.extensions['.tmpl'] = function (module, filename) {
5 | module.exports = fs.readFileSync(filename, 'utf8')
6 | }
7 |
8 | const modulesTemplate = handlebars.compile(require('../files/modules.xml.tmpl'))
9 | const moduleImlTemplate = handlebars.compile(require('../files/module.iml.tmpl'))
10 | const rootModuleImlTemplate = handlebars.compile(require('../files/root_module.iml.tmpl'))
11 | const workspaceXmlTemplate = handlebars.compile(require('../files/workspace.xml.tmpl'))
12 |
13 | module.exports.ideaModulesFile = function (targetFile, modules, options) {
14 | const content = modulesTemplate({modules: modules, options})
15 | fs.writeFileSync(targetFile, content)
16 | }
17 |
18 | module.exports.ideaModuleImlFile = function (targetFile, config) {
19 | const content = moduleImlTemplate({config})
20 | fs.writeFileSync(targetFile, content)
21 | }
22 |
23 | module.exports.ideaRootModuleImlFile = function (targetFile) {
24 | const content = rootModuleImlTemplate({})
25 | fs.writeFileSync(targetFile, content)
26 | }
27 |
28 | module.exports.ideaWorkspaceXmlFile = function (targetFile, config) {
29 | const content = workspaceXmlTemplate({config})
30 | fs.writeFileSync(targetFile, content)
31 | }
32 |
--------------------------------------------------------------------------------
/tasks/depcheck/index.js:
--------------------------------------------------------------------------------
1 | const {loadPackages, iter} = require('lerna-script')
2 | const checkDeps = require('depcheck')
3 | const colors = require('colors')
4 |
5 | function depcheckTask({packages, depcheck} = {}) {
6 | return async log => {
7 | const lernaPackages = await (packages || loadPackages())
8 | log.info('depcheck', `checking dependencies for ${lernaPackages.length} modules`)
9 |
10 | return iter.parallel(lernaPackages, {build: 'depcheck'})(lernaPackage => {
11 | return checkModule(lernaPackage, depcheck)
12 | })
13 | }
14 | }
15 |
16 | function checkModule(lernaPackage, depcheckOpts = {}) {
17 | return Promise.resolve()
18 | .then(() => checkDeps(lernaPackage.location, depcheckOpts, val => val))
19 | .then(({dependencies, devDependencies}) => {
20 | const hasUnusedDeps = devDependencies.concat(dependencies).length > 0
21 | if (hasUnusedDeps) {
22 | console.log(`\nunused deps found for module ${colors.brightCyan.bold(lernaPackage.name)}`)
23 | if (dependencies.length > 0) {
24 | console.log({dependencies})
25 | }
26 | if (devDependencies.length > 0) {
27 | console.log({devDependencies})
28 | }
29 | return Promise.reject(new Error(`unused deps found for module ${lernaPackage.name}`))
30 | }
31 | return Promise.resolve()
32 | })
33 | }
34 |
35 | module.exports = depcheckTask
36 |
--------------------------------------------------------------------------------
/tasks/modules/README.md:
--------------------------------------------------------------------------------
1 | # lerna-script-tasks-modules
2 |
3 | Syncs dependencies/devDependencies/peerDependencies for modules within repo.
4 |
5 | ## install
6 |
7 | ```bash
8 | npm install --save-dev lerna-script-tasks-modules
9 | ```
10 |
11 | ## Usage
12 |
13 | Say you have modules:
14 |
15 | - `/packages/a` with version `1.0.0`
16 | - `/packages/b` with version `1.0.0` and it depends on module `a` where `{dependencies: {"a": "~1.0.0"}}`
17 |
18 | and you up the version of `/packages/a` to `2.0.0`. If you want for version of `a` to be in sync in module `b`, then you could do:
19 |
20 | ```js
21 | //lerna.js
22 | const syncModules = require('lerna-script-tasks-modules')
23 |
24 | module.exports['modules:sync'] = syncModules()
25 | ```
26 |
27 | and then upon executing `lerna-script modules:sync` version of dependency `a` for module `b` will be set to `~2.0.0`.
28 | Same goes for `devDependencies` and `peerDependencies`.
29 |
30 | ## API
31 |
32 | ### ({packages: [], transformDependencies: version => version, transformPeerDependencies: version => version})(log): Promise
33 |
34 | Returns a function that syncs module versions across repo.
35 |
36 | Parameters:
37 |
38 | - packages, optional = list of lerna packages. Loads defaults of not provided.
39 | - transformDependencies, optional = function to transform dependencies and devDependencies. Defaults to `version => '~' + version`.
40 | - transformPeerDependencies, optional - function to transform peerDependencies. Defaults to `version => '>=' + version`.
41 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "lerna": "2.0.0",
3 | "bootstrap": {
4 | "npmClientArgs": ["--no-package-lock", "--no-ci"]
5 | },
6 | "packages": ["lerna-script", "tasks/*", "test-utils"],
7 | "version": "1.4.0",
8 | "managedDependencies": {
9 | "@lerna/batch-packages": "~3.16.0",
10 | "@lerna/child-process": "~3.16.0",
11 | "@lerna/collect-updates": "~3.20.0",
12 | "@lerna/filter-packages": "~3.18.0",
13 | "@lerna/npm-run-script": "~3.16.0",
14 | "@lerna/package": "~3.16.0",
15 | "@lerna/package-graph": "~3.18.0",
16 | "@lerna/project": "~3.21.0",
17 | "@lerna/run-parallel-batches": "3.16.0",
18 | "bdd-stdin": "0.2.0",
19 | "bluebird": "3.7.2",
20 | "chai": "4.2.0",
21 | "colors": "1.4.0",
22 | "deep-keys": "0.5.0",
23 | "depcheck": "0.9.2",
24 | "exec-then": "1.3.1",
25 | "fs-extra": "8.1.0",
26 | "git-remote-url": "1.0.1",
27 | "handlebars": "4.7.6",
28 | "hosted-git-info": "3.0.4",
29 | "ignore": "5.1.6",
30 | "inquirer": "^7.0.0",
31 | "intercept-stdout": "0.1.2",
32 | "invert-promise": "^1.0.1",
33 | "lerna": "3.21.0",
34 | "lodash": "4.17.15",
35 | "mocha": "7.2.0",
36 | "npmlog": "4.1.2",
37 | "ramda": "0.27.0",
38 | "sanitize-filename": "1.6.3",
39 | "semver": "7.3.2",
40 | "shelljs": "0.8.4",
41 | "sinon": "9.0.2",
42 | "sinon-chai": "3.5.0",
43 | "yargs": "^15.0.0"
44 | },
45 | "managedPeerDependencies": {
46 | "lerna": "~3.21.0",
47 | "sinon": ">9.0.0"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/lerna-script/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lerna-script",
3 | "version": "1.4.0",
4 | "description": "lerna extension for custom scripts",
5 | "main": "index.js",
6 | "homepage": "https://github.com/wix/lerna-script/tree/master/lerna-script",
7 | "bin": {
8 | "lerna-script": "bin/cli.js"
9 | },
10 | "scripts": {
11 | "test": "mocha"
12 | },
13 | "keywords": [
14 | "lerna",
15 | "script"
16 | ],
17 | "repository": {
18 | "type": "git",
19 | "url": "git@github.com:wix/lerna-script.git",
20 | "directory": "/lerna-script"
21 | },
22 | "author": "vilius@wix.com",
23 | "license": "BSD",
24 | "devDependencies": {
25 | "chai": "4.2.0",
26 | "intercept-stdout": "0.1.2",
27 | "invert-promise": "^1.0.1",
28 | "lerna-script-test-utils": "~1.3.2",
29 | "mocha": "7.2.0",
30 | "sinon": "9.0.2",
31 | "sinon-chai": "3.5.0"
32 | },
33 | "dependencies": {
34 | "@lerna/batch-packages": "~3.16.0",
35 | "@lerna/child-process": "~3.16.0",
36 | "@lerna/collect-updates": "~3.20.0",
37 | "@lerna/filter-packages": "~3.18.0",
38 | "@lerna/npm-run-script": "~3.16.0",
39 | "@lerna/package": "~3.16.0",
40 | "@lerna/package-graph": "~3.18.0",
41 | "@lerna/project": "~3.21.0",
42 | "@lerna/run-parallel-batches": "3.16.0",
43 | "bluebird": "3.7.2",
44 | "fs-extra": "8.1.0",
45 | "ignore": "5.1.6",
46 | "lodash": "4.17.15",
47 | "npmlog": "4.1.2",
48 | "sanitize-filename": "1.6.3",
49 | "shelljs": "0.8.4",
50 | "yargs": "^15.0.0"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/tasks/idea/files/workspace.xml.tmpl:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | $PROJECT_DIR$/{{config.mochaPackage}}
5 |
6 |
7 | {{#each config.modules}}
8 | {{#each this.mocha}}
9 |
10 |
11 | {{../nodePath}}
12 | $PROJECT_DIR$/{{../relativePath}}
13 | true
14 | {{#if this.environmentVariables}}
15 |
16 | {{#each this.environmentVariables}}
17 |
18 | {{/each}}
19 |
20 | {{/if}}
21 | bdd
22 | {{this.extraOptions}}
23 | {{this.testKind}}
24 | {{this.testPattern}}
25 |
26 |
27 | {{/each}}
28 | {{/each}}
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/lerna-script/bin/cli.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | const {readFileSync} = require('fs'),
3 | {join} = require('path'),
4 | taskRunner = require('../lib/task-runner'),
5 | log = require('npmlog')
6 |
7 | const argv = require('yargs')
8 | .usage('Usage: $0 [options] ')
9 | .option('loglevel', {
10 | describe: 'choose log level',
11 | choices: ['silly', 'verbose', 'info', 'warn', 'error']
12 | })
13 | .default('loglevel', 'info')
14 | .demandCommand(1)
15 | .help('help').argv
16 |
17 | log.level = argv.loglevel
18 | log.enableProgress()
19 | log.enableColor()
20 |
21 | const tasks = resolveTasksFile()
22 | const taskName = argv._[0]
23 |
24 | taskRunner({process, log})(tasks, taskName).catch(e => {
25 | log.error('lerna-script', `Task "${taskName}" failed.`, e)
26 | process.exit(1)
27 | })
28 |
29 | function resolveTasksFile() {
30 | log.verbose('Resolving lerna-script tasks file...')
31 | const lernaJson = JSON.parse(readFileSync('./lerna.json', 'utf8'))
32 | if (lernaJson['lerna-script-tasks']) {
33 | const tasks = lernaJson['lerna-script-tasks']
34 | log.verbose('lerna-script tasks defined in lerna.json, loading', {
35 | cwd: process.cwd(),
36 | 'lerna-script-tasks': tasks
37 | })
38 | const tasksOrFunction = require(tasks.startsWith('./') ? join(process.cwd(), tasks) : tasks)
39 |
40 | return typeof tasksOrFunction === 'function' ? tasksOrFunction() : tasksOrFunction
41 | } else {
42 | log.verbose('lerna-script tasks not defined in lerna.json, using defaults', {
43 | cwd: process.cwd(),
44 | 'lerna-script-tasks': 'lerna.js'
45 | })
46 | return require(join(process.cwd(), './lerna.js'))
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/lerna.js:
--------------------------------------------------------------------------------
1 | const {loadRootPackage, loadPackages, iter, exec} = require('lerna-script'),
2 | idea = require('lerna-script-tasks-idea'),
3 | syncModules = require('lerna-script-tasks-modules'),
4 | npmfix = require('lerna-script-tasks-npmfix'),
5 | dependencies = require('lerna-script-tasks-dependencies'),
6 | depcheck = require('lerna-script-tasks-depcheck')
7 |
8 | async function test(log) {
9 | const packages = await loadPackages()
10 | return iter.forEach(packages, {log, build: 'test'})((lernaPackage, log) => {
11 | return exec.script(lernaPackage, {log, silent: false})('test')
12 | })
13 | }
14 |
15 | async function clean(log) {
16 | const rootPackage = await loadRootPackage()
17 | const packages = await loadPackages()
18 | return exec
19 | .command(rootPackage, {log})('lerna clean --yes')
20 | .then(() => {
21 | return iter.forEach(packages.join([loadRootPackage()]), {log})((lernaPackage, log) => {
22 | const execCmd = cmd => exec.command(lernaPackage, {log})(cmd)
23 | return Promise.all(
24 | [
25 | 'rm -f *.log',
26 | 'rm -f *.log.*',
27 | 'rm -f yarn.lock',
28 | 'rm -f package-lock.json',
29 | 'rm -rf .lerna'
30 | ].map(execCmd)
31 | )
32 | })
33 | })
34 | }
35 |
36 | function sync(log) {
37 | return Promise.resolve()
38 | .then(() => syncModules()(log))
39 | .then(() => npmfix()(log))
40 | .then(() => dependencies.sync()(log))
41 | }
42 |
43 | module.exports = {
44 | test,
45 | sync,
46 | idea: idea(),
47 | depcheck: depcheck(),
48 | clean,
49 | 'deps:unmanaged': dependencies.unmanaged(),
50 | 'deps:latest': dependencies.latest(),
51 | 'deps:sync': dependencies.sync()
52 | }
53 |
--------------------------------------------------------------------------------
/lerna-script/test/packages.spec.js:
--------------------------------------------------------------------------------
1 | const {expect} = require('chai').use(require('sinon-chai')),
2 | {aLernaProjectWith2Modules, aLernaProject, loggerMock} = require('lerna-script-test-utils'),
3 | index = require('..'),
4 | sinon = require('sinon')
5 |
6 | describe('packages', () => {
7 | describe('loadPackages', () => {
8 | it('should return a list of packages', async () => {
9 | const log = loggerMock()
10 | const project = await aLernaProjectWith2Modules()
11 |
12 | return project.within(async () => {
13 | const packages = await index.loadPackages({log})
14 |
15 | expect(packages.length).to.equal(2)
16 | expect(log.verbose).to.have.been.calledWithMatch(
17 | 'loadPackages',
18 | 'using default packageConfigs'
19 | )
20 | })
21 | })
22 |
23 | it('should return topo-sorted packages', async () => {
24 | const project = await aLernaProject({
25 | a: ['b'],
26 | b: ['c'],
27 | c: ['d'],
28 | d: []
29 | })
30 | return project.within(async () => {
31 | const packages = await index.loadPackages()
32 | expect(packages.map(p => p.name)).to.deep.equal(['d', 'c', 'b', 'a'])
33 | })
34 | })
35 | })
36 |
37 | describe('loadRootPackage', () => {
38 | it('should return a root package', async () => {
39 | const log = loggerMock()
40 | const project = await aLernaProjectWith2Modules()
41 |
42 | return project.within(async () => {
43 | const rootPackage = await index.loadRootPackage({log})
44 |
45 | expect(rootPackage.name).to.equal('root')
46 | expect(rootPackage.location).to.equal(process.cwd())
47 | expect(log.verbose).to.have.been.calledWithMatch('loadRootPackage', sinon.match.object)
48 | })
49 | })
50 | })
51 | })
52 |
--------------------------------------------------------------------------------
/tasks/dependencies/lib/sync.js:
--------------------------------------------------------------------------------
1 | const {iter, fs, loadPackages} = require('lerna-script'),
2 | _ = require('lodash'),
3 | deepKeys = require('deep-keys')
4 |
5 | //TODO: logging for task
6 | function syncDependenciesTask({packages} = {}) {
7 | return log => {
8 | const lernaPackages = packages || loadPackages()
9 | log.info('sync', `syncing dependencies for ${lernaPackages.length} modules`)
10 | const template = asDependencies(require(process.cwd() + '/lerna.json'))
11 |
12 | return iter.parallel(lernaPackages, {log})((lernaPackage, log) => {
13 | const logMerged = input =>
14 | log.info(
15 | 'sync',
16 | `${lernaPackage.name}: ${input.key} (${input.currentValue} -> ${input.newValue})`
17 | )
18 |
19 | return fs
20 | .readFile(lernaPackage)('package.json', JSON.parse)
21 | .then(packageJson => {
22 | const synced = merge(packageJson, template, logMerged)
23 | return fs.writeFile(lernaPackage)('package.json', synced)
24 | })
25 | })
26 | }
27 | }
28 |
29 | function asDependencies({managedDependencies, managedPeerDependencies}) {
30 | return {
31 | dependencies: managedDependencies,
32 | devDependencies: managedDependencies,
33 | peerDependencies: managedPeerDependencies
34 | }
35 | }
36 |
37 | function merge(dest, source, onMerged = _.noop) {
38 | const destKeys = deepKeys(dest)
39 | const sourceKeys = deepKeys(source)
40 | const sharedKeys = _.intersection(destKeys, sourceKeys)
41 |
42 | sharedKeys.forEach(key => {
43 | const currentValue = _.get(dest, key)
44 | const newValue = _.get(source, key)
45 | if (currentValue !== newValue) {
46 | _.set(dest, key, newValue)
47 | onMerged({key, currentValue, newValue})
48 | }
49 | })
50 |
51 | return dest
52 | }
53 |
54 | module.exports.task = syncDependenciesTask
55 |
--------------------------------------------------------------------------------
/lerna-script/test/task-runner.spec.js:
--------------------------------------------------------------------------------
1 | const {expect} = require('chai').use(require('sinon-chai')),
2 | {loggerMock} = require('lerna-script-test-utils'),
3 | taskRunner = require('../lib/task-runner'),
4 | sinon = require('sinon')
5 |
6 | describe('task-runner', () => {
7 | it('should run provided script', () => {
8 | const {logMock, runTask} = setupMocks()
9 | const task = sinon.spy()
10 |
11 | return runTask({task}, 'task').then(() => {
12 | expect(task).to.have.been.calledWith(logMock)
13 | expect(logMock.info).to.have.been.calledWith('lerna-script', 'executing task: "task"')
14 | })
15 | })
16 |
17 | it('should log error, available tasks and exit with code 1 if provided task is not present', () => {
18 | const {logMock, processMock, runTask} = setupMocks()
19 |
20 | runTask({task1: '', task2: ''}, 'non-existent')
21 |
22 | expect(processMock.exit).to.have.been.calledWith(1)
23 | expect(logMock.error).to.have.been.calledWithMatch(
24 | 'lerna-script',
25 | 'Unable to find task "non-existent"'
26 | )
27 | expect(logMock.error).to.have.been.calledWithMatch(
28 | 'lerna-script',
29 | 'Available tasks: "task1", "task2"'
30 | )
31 | })
32 |
33 | it('should log available tasks and exit with 0 if no task is provided', () => {
34 | const {logMock, processMock, runTask} = setupMocks()
35 |
36 | runTask({task1: '', task2: ''})
37 |
38 | expect(processMock.exit).to.have.been.calledWith(0)
39 | expect(logMock.info).to.have.been.calledWithMatch(
40 | 'lerna-script',
41 | 'No task provided.',
42 | 'Available tasks: "task1", "task2"'
43 | )
44 | })
45 |
46 | function setupMocks() {
47 | const logMock = loggerMock()
48 | const processMock = {
49 | exit: sinon.spy()
50 | }
51 |
52 | return {logMock, processMock, runTask: taskRunner({log: logMock, process: processMock})}
53 | }
54 | })
55 |
--------------------------------------------------------------------------------
/tasks/npmfix/index.js:
--------------------------------------------------------------------------------
1 | const {loadPackages, iter, fs} = require('lerna-script'),
2 | gitRemoteUrl = require('git-remote-url'),
3 | gitInfo = require('hosted-git-info'),
4 | {relative} = require('path')
5 |
6 | function sortByKey(obj) {
7 | const sorted = {}
8 | Object.keys(obj)
9 | .sort()
10 | .forEach(key => (sorted[key] = obj[key]))
11 | return sorted
12 | }
13 |
14 | function sortDependencies(deps) {
15 | if (deps) {
16 | return sortByKey(deps)
17 | }
18 | }
19 |
20 | function npmfix({packages} = {}) {
21 | return async log => {
22 | const lernaPackages = await (packages || loadPackages())
23 | log.info('npmfix', `fixing homepage, repo urls for ${lernaPackages.length} packages`)
24 |
25 | return gitRemoteUrl('.', 'origin').then(gitRemoteUrl => {
26 | const info = gitInfo.fromUrl(gitRemoteUrl)
27 | const browseUrl = info.browse()
28 | const repoUrl = info.ssh()
29 |
30 | return iter.parallel(lernaPackages, {log})((lernaPackage, log) => {
31 | const moduleRelativePath = relative(process.cwd(), lernaPackage.location)
32 |
33 | return fs
34 | .readFile(lernaPackage, {log})('package.json', JSON.parse)
35 | .then(packageJson => {
36 | const updated = Object.assign({}, packageJson, {
37 | homepage: browseUrl + '/tree/master/' + moduleRelativePath,
38 | dependencies: sortDependencies(packageJson.dependencies),
39 | devDependencies: sortDependencies(packageJson.devDependencies),
40 | peerDependencies: sortDependencies(packageJson.peerDependencies),
41 | repository: {
42 | type: 'git',
43 | url: repoUrl,
44 | ...(moduleRelativePath && {directory: '/' + moduleRelativePath})
45 | }
46 | })
47 | return fs.writeFile(lernaPackage, {log})('package.json', updated)
48 | })
49 | })
50 | })
51 | }
52 | }
53 |
54 | module.exports = npmfix
55 |
--------------------------------------------------------------------------------
/lerna-script/lib/exec.js:
--------------------------------------------------------------------------------
1 | const runNpmScript = require('@lerna/npm-run-script'),
2 | {exec, spawnStreaming} = require('@lerna/child-process'),
3 | npmlog = require('npmlog')
4 |
5 | function dirtyMaxListenersErrorHack() {
6 | process.stdout.on('close', () => {})
7 | process.stdout.on('close', () => {})
8 | process.stdout.on('close', () => {})
9 | }
10 |
11 | function runCommand(lernaPackage, {silent = true, log = npmlog} = {silent: true, log: npmlog}) {
12 | return command => {
13 | log.silly('runCommand', command, {cwd: lernaPackage.location, silent})
14 | const commandAndArgs = command.split(' ')
15 | const actualCommand = commandAndArgs.shift()
16 | const actualCommandArgs = commandAndArgs
17 | // return new Promise((resolve, reject) => {
18 | // const callback = (err, stdout) => (err ? reject(err) : resolve(stdout))
19 | if (silent) {
20 | return Promise.resolve()
21 | .then(() => exec(actualCommand, [...actualCommandArgs], {cwd: lernaPackage.location}))
22 | .then(res => res.stdout)
23 | } else {
24 | dirtyMaxListenersErrorHack()
25 |
26 | return spawnStreaming(
27 | actualCommand,
28 | [...actualCommandArgs],
29 | {cwd: lernaPackage.location},
30 | lernaPackage.name
31 | ).then(res => res.stdout)
32 | }
33 | }
34 | }
35 |
36 | function runScript(lernaPackage, {silent = true, log = npmlog} = {silent: true, log: npmlog}) {
37 | return script => {
38 | if (lernaPackage.scripts && lernaPackage.scripts[script]) {
39 | if (silent) {
40 | return runNpmScript(script, {args: [], pkg: lernaPackage, npmClient: 'npm'}).then(
41 | res => res.stdout
42 | )
43 | } else {
44 | dirtyMaxListenersErrorHack()
45 |
46 | return runNpmScript
47 | .stream(script, {args: [], pkg: lernaPackage, npmClient: 'npm'})
48 | .then(res => res.stdout)
49 | }
50 | } else {
51 | log.warn('runNpmScript', 'script not found', {script, cwd: lernaPackage.location})
52 | return Promise.resolve('')
53 | }
54 | }
55 | }
56 |
57 | module.exports = {
58 | runCommand,
59 | runScript
60 | }
61 |
--------------------------------------------------------------------------------
/tasks/dependencies/test/extraneous.it.js:
--------------------------------------------------------------------------------
1 | const {aLernaProject, loggerMock} = require('lerna-script-test-utils'),
2 | {expect} = require('chai').use(require('sinon-chai')),
3 | {loadPackages} = require('lerna-script'),
4 | {extraneous} = require('..')
5 |
6 | describe('extraneous task', () => {
7 | it('should list dependencies present in managed*Dependencies, but not in modules', async () => {
8 | const {log, project} = await setup()
9 |
10 | return project.within(() => {
11 | return extraneous()(log).then(() => {
12 | expect(log.error).to.have.been.calledWith(
13 | 'extraneous',
14 | 'managedDependencies: adash, highdash'
15 | )
16 | expect(log.error).to.have.been.calledWith('extraneous', 'managedPeerDependencies: bar')
17 | })
18 | })
19 | })
20 |
21 | it('should use packages if provided', async () => {
22 | const {log, project} = await setup()
23 |
24 | return project.within(async () => {
25 | const packages = await loadPackages()
26 | const filteredPackages = packages.filter(p => p.name === 'b')
27 |
28 | return extraneous({packages: filteredPackages})(log).then(() => {
29 | expect(log.error).to.have.been.calledWith(
30 | 'extraneous',
31 | 'managedDependencies: adash, highdash'
32 | )
33 | expect(log.error).to.have.been.calledWith('extraneous', 'managedPeerDependencies: bar, foo')
34 | })
35 | })
36 | })
37 |
38 | async function setup() {
39 | const log = loggerMock()
40 | const project = await aLernaProject()
41 |
42 | project
43 | .lernaJson({
44 | managedDependencies: {lodash: '1.1.0', highdash: '1.1.0', adash: '1.1.0'},
45 | managedPeerDependencies: {foo: '> 1.0.0', bar: '> 1.0.0'}
46 | })
47 | .module('packages/a', module =>
48 | module.packageJson({
49 | name: 'a',
50 | version: '1.0.0',
51 | peerDependencies: {foo: '1'},
52 | devDependencies: {lodash: 'nope'}
53 | })
54 | )
55 | .module('packages/b', module =>
56 | module.packageJson({
57 | version: '1.0.0',
58 | dependencies: {a: '~1.0.0', lodash: '~1.0.0'}
59 | })
60 | )
61 |
62 | return {project, log}
63 | }
64 | })
65 |
--------------------------------------------------------------------------------
/tasks/idea/README.md:
--------------------------------------------------------------------------------
1 | # lerna-script-tasks-idea
2 |
3 | [lerna-script](../../lerna-script) task to generate [WebStorm](https://www.jetbrains.com/webstorm/) project for a [Lerna](https://lernajs.io/) managed project with hardcoded conventions:
4 |
5 | - mark `node_modules` as ignored so [WebStorm](https://www.jetbrains.com/webstorm/) would not index those. Having >= 20 modules open with `node_modules` indexing pretty much kills it:/
6 | - set source level to `es6`;
7 | - mark `lib`, `src` as source rootps and `test`, `tests` as test roots;
8 | - add [mocha](https://mochajs.org/) run configurations for all modules.
9 |
10 | **Note:** given this task generates [WebStorm](https://www.jetbrains.com/webstorm/) project files manually, you must close all instances of [WebStorm](https://www.jetbrains.com/webstorm/) before generating and open afterwards.
11 |
12 | ## install
13 |
14 | ```bash
15 | npm install --save-dev lerna-script-tasks-idea
16 | ```
17 |
18 | ## Usage
19 |
20 | Add `lerna-script` launcher to `package.json` scripts:
21 |
22 | ```json
23 | {
24 | "scripts": {
25 | "start": "lerna-script"
26 | }
27 | }
28 | ```
29 |
30 | Add export to `lerna.js`:
31 |
32 | ```js
33 | const idea = require('lerna-script-tasks-idea');
34 |
35 | module.exports.idea = idea();
36 | ```
37 |
38 | To generate [WebStorm](https://www.jetbrains.com/webstorm/) project run:
39 |
40 | ```bash
41 | npm start idea
42 | ```
43 |
44 | # API
45 |
46 | ## ({[packages], mochaConfigurations: packageJson => [], excludePatterns, addRoot: boolean = false})(log): Promise
47 |
48 | Returns a function that generates [WebStorm](https://www.jetbrains.com/webstorm/) for all modules in repo.
49 |
50 | Parameters:
51 |
52 | - packages - list of packages to generate idea project for or defaults to ones defined in `lerna.json`;
53 | - mochaConfigurations - function, that, given packageJson object of a module returns a list of mocha configurations in a format:
54 | - name - configuration name;
55 | - environmentVariables - key/value pair of environment variables for configuration;
56 | - extraOptions - extra mocha options;
57 | - testKind - kind of test, ex. PATTERN;
58 | - testPattern - pattern expression.
59 | - excludePatterns - array of patterns that will be set as the project exclude patterns. Files\Folders matching that pattern will be marked as "excluded" in Idea
60 | - addRoot - when true, the `root.iml` file will be generated to make all non-modules visible in IDEA (_optional, defaults to false_)
61 | - log - `npmlog` instance.
62 |
--------------------------------------------------------------------------------
/tasks/dependencies/lib/extraneous.js:
--------------------------------------------------------------------------------
1 | const {iter, fs, loadPackages} = require('lerna-script'),
2 | R = require('ramda')
3 |
4 | //TODO: logging for task
5 | function extraneousDependenciesTask({packages} = {}) {
6 | return log => {
7 | const lernaPackages = packages || loadPackages()
8 | log.info(
9 | 'extraneous',
10 | `checking for extraneous dependencies for ${lernaPackages.length} modules`
11 | )
12 | const deps = {dependencies: {}, peerDependencies: {}}
13 | const {managedDependencies = {}, managedPeerDependencies = {}} = require(process.cwd() +
14 | '/lerna.json')
15 | const readJson = lernaPackage => fs.readFile(lernaPackage)('package.json', JSON.parse)
16 |
17 | return iter
18 | .parallel(lernaPackages, {log})(readJson)
19 | .then(packageJsons => {
20 | packageJsons.forEach(packageJson => fillModulesAndDeps(deps, packageJson))
21 | executeExtraneous(managedDependencies, managedPeerDependencies, deps, log)
22 | })
23 | }
24 | }
25 |
26 | function executeExtraneous(managedDependencies, managedPeerDependencies, deps, log) {
27 | cleanManagedDeps(deps, managedDependencies, managedPeerDependencies)
28 | logExtraneous({managedDependencies}, log, 'managedDependencies')
29 | logExtraneous({managedPeerDependencies}, log, 'managedPeerDependencies')
30 | }
31 |
32 | function logExtraneous(deps, log, dependencyType) {
33 | const managedDependencies = deps[dependencyType]
34 | const toSortedUniqKeys = R.compose(
35 | R.sort((a, b) => a.localeCompare(b)),
36 | R.uniq,
37 | R.keys
38 | )
39 | const modules = toSortedUniqKeys(managedDependencies)
40 | if (modules.length > 0) {
41 | log.error('extraneous', `${dependencyType}: ${modules.join(', ')}`)
42 | }
43 | }
44 |
45 | function cleanManagedDeps(deps, managedDependencies, managedPeerDependencies) {
46 | Object.keys(deps.dependencies || {}).forEach(name => delete managedDependencies[name])
47 | Object.keys(deps.devDependencies || {}).forEach(name => delete managedDependencies[name])
48 | Object.keys(deps.peerDependencies || {}).forEach(name => delete managedPeerDependencies[name])
49 | }
50 |
51 | function fillModulesAndDeps(deps, packageJson) {
52 | Object.assign(deps.dependencies, packageJson.dependencies)
53 | Object.assign(deps.dependencies, packageJson.devDependencies)
54 | Object.assign(deps.peerDependencies, packageJson.peerDependencies)
55 | }
56 |
57 | module.exports.task = extraneousDependenciesTask
58 | module.exports.logExtraneous = logExtraneous
59 |
--------------------------------------------------------------------------------
/tasks/dependencies/test/unmanaged.it.js:
--------------------------------------------------------------------------------
1 | const {aLernaProject, loggerMock} = require('lerna-script-test-utils'),
2 | {expect} = require('chai').use(require('sinon-chai')),
3 | {loadPackages} = require('lerna-script'),
4 | {unmanaged} = require('..')
5 |
6 | describe('unmanaged task', () => {
7 | it('should list dependencies present in modules, but not in managed*Dependencies', async () => {
8 | const {log, project} = await setup()
9 |
10 | return project.within(() => {
11 | return unmanaged()(log).then(() => {
12 | expect(log.error).to.have.been.calledWith(
13 | 'unmanaged',
14 | 'unmanaged dependency highdash (1.1.0, 1.2.0)'
15 | )
16 | expect(log.error).to.have.been.calledWith(
17 | 'unmanaged',
18 | 'unmanaged peerDependency bar (> 1.0.0)'
19 | )
20 | })
21 | })
22 | })
23 |
24 | it('should use packages if provided', async () => {
25 | const {log, project} = await setup()
26 |
27 | return project.within(async () => {
28 | const packages = await loadPackages()
29 | const filteredPackages = packages.filter(p => p.name === 'a')
30 |
31 | return unmanaged({packages: filteredPackages})(log).then(() => {
32 | expect(log.error).to.have.been.calledWith(
33 | 'unmanaged',
34 | 'unmanaged dependency highdash (1.1.0)'
35 | )
36 | expect(log.error).to.have.been.calledWith(
37 | 'unmanaged',
38 | 'unmanaged peerDependency bar (> 1.0.0)'
39 | )
40 | })
41 | })
42 | })
43 |
44 | async function setup() {
45 | const log = loggerMock()
46 | const project = await aLernaProject()
47 | project
48 | .lernaJson({
49 | managedDependencies: {
50 | lodash: '1.1.0'
51 | },
52 | managedPeerDependencies: {
53 | foo: '> 1.0.0'
54 | }
55 | })
56 | .module('packages/a', module =>
57 | module.packageJson({
58 | name: 'a',
59 | version: '1.0.0',
60 | peerDependencies: {
61 | foo: '1',
62 | bar: '> 1.0.0'
63 | },
64 | devDependencies: {
65 | lodash: 'nope',
66 | highdash: '1.1.0'
67 | }
68 | })
69 | )
70 | .module('packages/b', module =>
71 | module.packageJson({
72 | version: '1.0.0',
73 | dependencies: {
74 | a: '~1.0.0',
75 | lodash: '~1.0.0',
76 | highdash: '1.2.0'
77 | }
78 | })
79 | )
80 |
81 | return {log, project}
82 | }
83 | })
84 |
--------------------------------------------------------------------------------
/tasks/modules/index.js:
--------------------------------------------------------------------------------
1 | const {loadPackages, iter, fs} = require('lerna-script'),
2 | _ = require('lodash'),
3 | deepKeys = require('deep-keys')
4 |
5 | function syncModulesTask({packages, transformDependencies, transformPeerDependencies} = {}) {
6 | return async log => {
7 | const {loadedPackages, transformDeps, transformPeerDeps} = await providedOrDefaults({
8 | packages,
9 | transformDependencies,
10 | transformPeerDependencies
11 | })
12 |
13 | log.info('modules', `syncing module versions for ${loadedPackages.length} packages`)
14 | const modulesAndVersions = toModulesAndVersion(loadedPackages, transformDeps)
15 | const modulesAndPeerVersions = toModulesAndVersion(loadedPackages, transformPeerDeps)
16 | return iter.parallel(loadedPackages, {log})((lernaPackage, log) => {
17 | const logMerged = input =>
18 | log.info(
19 | 'modules',
20 | `${lernaPackage.name}: ${input.key} (${input.currentValue} -> ${input.newValue})`
21 | )
22 | return fs
23 | .readFile(lernaPackage)('package.json', JSON.parse)
24 | .then(packageJson =>
25 | merge(
26 | packageJson,
27 | {
28 | dependencies: modulesAndVersions,
29 | devDependencies: modulesAndVersions,
30 | peerDependencies: modulesAndPeerVersions
31 | },
32 | logMerged
33 | )
34 | )
35 | .then(packageJson => fs.writeFile(lernaPackage)('package.json', packageJson))
36 | })
37 | }
38 | }
39 |
40 | async function providedOrDefaults({
41 | packages,
42 | transformDependencies,
43 | transformPeerDependencies
44 | } = {}) {
45 | return {
46 | loadedPackages: await (packages || loadPackages()),
47 | transformDeps: transformDependencies || (version => `~${version}`),
48 | transformPeerDeps: transformPeerDependencies || (version => `>=${version}`)
49 | }
50 | }
51 |
52 | function toModulesAndVersion(modules, mutateVersion) {
53 | return modules.reduce((acc, val) => {
54 | acc[val.name] = mutateVersion(val.version)
55 | return acc
56 | }, {})
57 | }
58 |
59 | function merge(dest, source, onMerged = _.noop) {
60 | const destKeys = deepKeys(dest)
61 | const sourceKeys = deepKeys(source)
62 | const sharedKeys = _.intersection(destKeys, sourceKeys)
63 |
64 | sharedKeys.forEach(key => {
65 | const currentValue = _.get(dest, key)
66 | const newValue = _.get(source, key)
67 | if (currentValue !== newValue) {
68 | _.set(dest, key, newValue)
69 | onMerged({key, currentValue, newValue})
70 | }
71 | })
72 |
73 | return dest
74 | }
75 |
76 | module.exports = syncModulesTask
77 |
--------------------------------------------------------------------------------
/lerna-script/test/fs.spec.js:
--------------------------------------------------------------------------------
1 | const {EOL} = require('os'),
2 | {expect} = require('chai'),
3 | {aLernaProjectWith2Modules} = require('lerna-script-test-utils'),
4 | index = require('..')
5 |
6 | describe('fs', () => {
7 | describe('readFile', () => {
8 | it('should read a file in module dir and return content as string', async () => {
9 | const project = await aLernaProjectWith2Modules()
10 | return project.within(async () => {
11 | const [lernaPackage] = await index.loadPackages()
12 |
13 | return index.fs
14 | .readFile(lernaPackage)('package.json')
15 | .then(fileContent => {
16 | expect(fileContent).to.be.string(`"name": "${lernaPackage.name}"`)
17 | })
18 | })
19 | })
20 |
21 | it('should read a file as json by providing custom converter', async () => {
22 | const project = await aLernaProjectWith2Modules()
23 | return project.within(async () => {
24 | const [lernaPackage] = await index.loadPackages()
25 |
26 | return index.fs
27 | .readFile(lernaPackage)('package.json', JSON.parse)
28 | .then(fileContent => {
29 | expect(fileContent).to.contain.property('name', lernaPackage.name)
30 | })
31 | })
32 | })
33 | })
34 |
35 | describe('writeFile', () => {
36 | it('should write string to file', async () => {
37 | const project = await aLernaProjectWith2Modules()
38 | return project.within(async () => {
39 | const [lernaPackage] = await index.loadPackages()
40 |
41 | return index.fs
42 | .writeFile(lernaPackage)('qwe.txt', 'bubu')
43 | .then(() => index.fs.readFile(lernaPackage)('qwe.txt'))
44 | .then(fileContent => expect(fileContent).to.equal('bubu'))
45 | })
46 | })
47 |
48 | it('should write object with a newline at the end of file', async () => {
49 | const project = await aLernaProjectWith2Modules()
50 | return project.within(async () => {
51 | const [lernaPackage] = await index.loadPackages()
52 |
53 | return index.fs
54 | .writeFile(lernaPackage)('qwe.json', {key: 'bubu'})
55 | .then(() => index.fs.readFile(lernaPackage)('qwe.json'))
56 | .then(fileContent => {
57 | expect(fileContent).to.match(new RegExp(`${EOL}$`))
58 | expect(JSON.parse(fileContent)).to.deep.equal({key: 'bubu'})
59 | })
60 | })
61 | })
62 |
63 | it('should accept custom serializer', async () => {
64 | const project = await aLernaProjectWith2Modules()
65 | return project.within(async () => {
66 | const [lernaPackage] = await index.loadPackages()
67 |
68 | return index.fs
69 | .writeFile(lernaPackage)('qwe.txt', 'bubu', c => 'a' + c)
70 | .then(() => index.fs.readFile(lernaPackage)('qwe.txt'))
71 | .then(fileContent => expect(fileContent).to.equal('abubu'))
72 | })
73 | })
74 | })
75 | })
76 |
--------------------------------------------------------------------------------
/tasks/dependencies/lib/unmanaged.js:
--------------------------------------------------------------------------------
1 | const {iter, fs, loadPackages} = require('lerna-script'),
2 | R = require('ramda')
3 |
4 | //TODO: logging
5 | function unmanagedDependenciesTask({packages} = {}) {
6 | return async log => {
7 | const lernaPackages = await (packages || loadPackages())
8 | log.info('unmanaged', `checking for unmanaged dependencies for ${lernaPackages.length} modules`)
9 | const deps = {dependencies: {}, peerDependencies: {}}
10 | const {managedDependencies = {}, managedPeerDependencies = {}} = require(process.cwd() +
11 | '/lerna.json')
12 | const innerModules = lernaPackages.map(p => p.name)
13 | const readJson = lernaPackage => fs.readFile(lernaPackage)('package.json', JSON.parse)
14 |
15 | return iter
16 | .parallel(lernaPackages, {log})(readJson)
17 | .then(packageJsons => {
18 | packageJsons.forEach(packageJson => fillModulesAndDeps(deps, packageJson))
19 | executeUnmanaged(managedDependencies, managedPeerDependencies, deps, innerModules, log)
20 | })
21 | }
22 | }
23 |
24 | function executeUnmanaged(managedDependencies, managedPeerDependencies, deps, innerModules, log) {
25 | cleanProjectDeps(innerModules, deps)
26 | cleanManagedDeps(deps, managedDependencies, managedPeerDependencies)
27 | logUnmanaged(deps, log)
28 | }
29 |
30 | function logUnmanaged(deps, log) {
31 | const toSortedUniqKeys = R.compose(R.sort(R.ascend), R.uniq, R.values)
32 | Object.keys(deps.dependencies).forEach(depKey => {
33 | const modulesAndVersions = toSortedUniqKeys(deps.dependencies[depKey])
34 | log.error('unmanaged', `unmanaged dependency ${depKey} (${modulesAndVersions.join(', ')})`)
35 | })
36 |
37 | Object.keys(deps.peerDependencies).forEach(depKey => {
38 | const modulesAndVersions = toSortedUniqKeys(deps.peerDependencies[depKey])
39 | log.error('unmanaged', `unmanaged peerDependency ${depKey} (${modulesAndVersions.join(', ')})`)
40 | })
41 | }
42 |
43 | function cleanProjectDeps(innerModules, deps) {
44 | innerModules.forEach(name => delete deps.dependencies[name])
45 | innerModules.forEach(name => delete deps.peerDependencies[name])
46 | }
47 |
48 | function cleanManagedDeps(deps, managedDependencies, managedPeerDependencies) {
49 | Object.keys(managedDependencies).forEach(name => delete deps.dependencies[name])
50 | Object.keys(managedPeerDependencies).forEach(name => delete deps.peerDependencies[name])
51 | }
52 |
53 | function fillModulesAndDeps(deps, packageJson) {
54 | fill(deps.dependencies)(packageJson, 'dependencies')
55 | fill(deps.dependencies)(packageJson, 'devDependencies')
56 | fill(deps.peerDependencies)(packageJson, 'peerDependencies')
57 | }
58 |
59 | function fill(deps) {
60 | return (packageJson, type) => {
61 | Object.keys(packageJson[type] || []).forEach(depKey => {
62 | deps[depKey] = deps[depKey] || {}
63 | deps[depKey][packageJson.name] = packageJson[type][depKey]
64 | })
65 | }
66 | }
67 |
68 | module.exports.task = unmanagedDependenciesTask
69 |
--------------------------------------------------------------------------------
/test-utils/index.js:
--------------------------------------------------------------------------------
1 | const {resolve} = require('path'),
2 | ModuleBuilder = require('./lib/module-builder'),
3 | {readFileSync, writeFileSync} = require('fs'),
4 | {join} = require('path'),
5 | fsExtra = require('fs-extra'),
6 | os = require('os'),
7 | sinon = require('sinon')
8 |
9 | const TEMP_DIR = os.tmpdir()
10 |
11 | function empty() {
12 | const projectDir = resolve(TEMP_DIR, Math.ceil(Math.random() * 100000).toString())
13 | afterEach(done => fsExtra.remove(projectDir, done))
14 | return new ModuleBuilder(process.cwd(), projectDir, true)
15 | }
16 |
17 | function readJson(name, dir = process.cwd()) {
18 | return JSON.parse(readFileSync(join(dir, name)).toString())
19 | }
20 |
21 | function readFile(name, dir = process.cwd()) {
22 | return readFileSync(join(dir, name)).toString()
23 | }
24 |
25 | function writeJson(name, content, dir = process.cwd()) {
26 | return writeFileSync(join(dir, name), JSON.stringify(content))
27 | }
28 |
29 | function aLernaProjectWith2Modules(moduleA = 'a') {
30 | return aLernaProject({[moduleA]: [], b: [moduleA]})
31 | }
32 |
33 | async function aLernaProject(spec = {}, overrides = {}) {
34 | const project = await empty()
35 | .packageJson({name: 'root'})
36 | .lernaJson()
37 | .inDir(ctx => ctx.exec('git init'))
38 |
39 | Object.keys(spec).forEach(name => {
40 | project.module(`packages/${stripScope(name)}`, module => {
41 | const dependencies = {}
42 | spec[name].forEach(dep => (dependencies[dep] = '1.0.0'))
43 | module.packageJson(Object.assign({name, version: '1.0.0', dependencies}, overrides))
44 | })
45 | })
46 |
47 | return project.inDir(ctx => ctx.exec('git init'))
48 | }
49 |
50 | function stripScope(name) {
51 | const sep = name.indexOf('/')
52 | return sep === -1 ? name : name.substring(sep + 1)
53 | }
54 |
55 | function loggerMock() {
56 | const item = {
57 | finish: sinon.spy(),
58 | completeWork: sinon.spy(),
59 | verbose: sinon.spy(),
60 | warn: sinon.spy(),
61 | silly: sinon.spy(),
62 | info: sinon.spy(),
63 | pause: sinon.spy(),
64 | error: sinon.spy(),
65 | resume: sinon.spy()
66 | }
67 |
68 | const group = {
69 | finish: sinon.spy(),
70 | verbose: sinon.spy(),
71 | warn: sinon.spy(),
72 | silly: sinon.spy(),
73 | info: sinon.spy(),
74 | error: sinon.spy(),
75 | newItem: sinon.stub().returns(item)
76 | }
77 |
78 | return {
79 | verbose: sinon.spy(),
80 | warn: sinon.spy(),
81 | silly: sinon.spy(),
82 | info: sinon.spy(),
83 | error: sinon.spy(),
84 | disableProgress: sinon.spy(),
85 | newItem: sinon.stub().returns(item),
86 | newGroup: sinon.stub().returns(group),
87 | item,
88 | group
89 | }
90 | }
91 |
92 | module.exports = {
93 | empty,
94 | aLernaProjectWith2Modules,
95 | aLernaProject,
96 | fs: {
97 | readJson,
98 | writeJson,
99 | readFile
100 | },
101 | loggerMock
102 | }
103 |
--------------------------------------------------------------------------------
/tasks/depcheck/test/depcheck.spec.js:
--------------------------------------------------------------------------------
1 | const {aLernaProject, loggerMock} = require('lerna-script-test-utils'),
2 | {loadPackages} = require('lerna-script'),
3 | {expect} = require('chai').use(require('sinon-chai')),
4 | depcheckTask = require('..'),
5 | invertPromise = require('invert-promise'),
6 | sinon = require('sinon'),
7 | colors = require('colors')
8 |
9 | describe('depcheck', () => {
10 | it('should fail for extraneous dependency', async () => {
11 | const log = loggerMock()
12 | console.log = sinon.spy()
13 | const project = await aLernaProject({a: ['lodash']})
14 |
15 | return project.within(() => {
16 | return invertPromise(
17 | depcheckTask()(log).then(() =>
18 | expect(log.info).to.have.been.calledWith(
19 | 'depcheck',
20 | 'checking dependencies for 1 modules'
21 | )
22 | )
23 | ).then(err => {
24 | expect(err.message).to.be.string('unused deps found for module a')
25 | expect(console.log.firstCall).to.have.been.calledWith(
26 | `\nunused deps found for module ${colors.brightCyan.bold('a')}`
27 | )
28 | expect(console.log).to.have.been.calledWithMatch({dependencies: ['lodash']})
29 | })
30 | })
31 | })
32 |
33 | it('should support custom packages', async () => {
34 | const log = loggerMock()
35 | const project = await aLernaProject({a: ['lodash'], b: []})
36 |
37 | return project.within(async () => {
38 | const packages = await loadPackages()
39 | const filteredPackages = packages.filter(p => p.name === 'b')
40 | return depcheckTask({packages: filteredPackages})(log)
41 | })
42 | })
43 |
44 | it('should pass for no extraneous dependencies', async () => {
45 | const log = loggerMock()
46 | const project = await aLernaProject({a: []})
47 |
48 | return project.within(() => depcheckTask()(log))
49 | })
50 |
51 | it('should respect provided overrides', async () => {
52 | const log = loggerMock()
53 | const project = await aLernaProject({a: ['lodash']})
54 |
55 | return project.within(() => depcheckTask({depcheck: {ignoreMatches: ['lodash']}})(log))
56 | })
57 |
58 | // it('build modules incrementally', () => {
59 | // const reporter = sinon.spy();
60 | // const project = empty()
61 | // .module('a', module => module.packageJson({version: '1.0.0'}))
62 | // .module('b', module => module.packageJson({version: '1.0.0'}));
63 | //
64 | // return project.within(() => {
65 | // return Promise.resolve()
66 | // .then(() => new Start(reporter)(depcheckTask()))
67 | // .then(() => expect(reporter).to.have.been.calledWith(sinon.match.any, sinon.match.any, 'Filtered-out 0 unchanged modules'))
68 | // .then(() => removeSync('a/target'))
69 | // .then(() => new Start(reporter)(depcheckTask()))
70 | // .then(() => expect(reporter).to.have.been.calledWith(sinon.match.any, sinon.match.any, 'Filtered-out 1 unchanged modules'));
71 | // });
72 | // });
73 | })
74 |
--------------------------------------------------------------------------------
/tasks/dependencies/README.md:
--------------------------------------------------------------------------------
1 | # lerna-script-tasks-dependencies
2 |
3 | [lerna-script](../..) tasks for managing dependency versions across lerna repo.
4 |
5 | ## install
6 |
7 | ```bash
8 | npm install --save-dev lerna-script-tasks-dependencies
9 | ```
10 |
11 | ## Usage
12 |
13 | TBD
14 |
15 | ```js
16 | const {extraneous, unmanaged, sync, latest} = require('lerna-script-tasks-dependencies');
17 |
18 | module.exports['deps:sync'] = sync();
19 | module.exports['deps:extraneous'] = extraneous();
20 | module.exports['deps:unmanaged'] = unmanaged();
21 | module.exports['deps:latest'] = latest();
22 | ```
23 |
24 | ## API
25 |
26 | ### sync({[packages]})(log): Promise
27 |
28 | Task that syncs dependency versions (dependencies, devDependencies, peerDependencies) with those defined in `lerna.json` as `managed*Dependencies`.
29 |
30 | Parameters:
31 |
32 | - packages - custom package list, or defaults as defined by `lerna.json`
33 | - log - `npmlog` instance passed-in by `lerna-script`;
34 |
35 | Say you have `lerna.json` in root of your project like:
36 |
37 | ```json
38 | {
39 | "managedDependencies": {
40 | "lodash": "~1.0.0"
41 | }
42 | }
43 | ```
44 |
45 | upon invocation of this task for all submodules that have `lodash` defined in `dependencies` or `devDependencies` version of `lodash` will be updated to `~1.0.0`.
46 |
47 | ### unmanaged({[packages]})(log): Promise
48 |
49 | List dependencies, that are present in modules `dependencies`, `devDependencies`, `peerDependencies`, but not defined in `lerna.json` as `managed*Dependencies`.
50 |
51 | Parameters:
52 |
53 | - packages - custom package list, or defaults as defined by `lerna.json`
54 | - log - `npmlog` instance passed-in by `lerna-script`;
55 |
56 | ### extraneous({[packages]})(log): Promise
57 |
58 | List dependencies, that are present in `lerna.json` as `managed*Dependencies`, but not defined in modules `dependencies`, `devDependencies`, `peerDependencies`.
59 |
60 | Parameters:
61 |
62 | - packages - custom package list, or defaults as defined by `lerna.json`
63 | - log - `npmlog` instance passed-in by `lerna-script`;
64 |
65 | ### latest({[addRange, silent]})(log): Promise
66 |
67 | List dependencies, that are present in `lerna.json` as `managed*Dependencies` and needs updating based on latest version published in npmjs.org.
68 | The `lerna.json` can contain the following `autoSelect` rules which will automatically mark the relevant packages as _selected_
69 |
70 | ```json
71 | {
72 | "managedDependencies": {
73 | "lodash": "~1.0.0",
74 | "dontUpdateMe": "3.0.5"
75 | },
76 | "autoselect": {
77 | "versionDiff": ["minor", "patch"],
78 | "exclude": ["dontUpdateMe"]
79 | }
80 | }
81 | ```
82 |
83 | In the above example, if a `minor` or a `patch` update is found for one of the packages, they will be selected by default unless the package name is `dontUpdateMe`
84 |
85 | Parameters:
86 |
87 | - addRange - when updating version in `lerna.json` to add range operator ('~', '^', ...). By default it sets fixed version.
88 | - silent - does not prompt with the list of dependencies and automatically updates auto-selected packages versions in the `lerna.json` file
89 | - log - `npmlog` instance passed-in by `lerna-script`;
90 |
--------------------------------------------------------------------------------
/lerna-script/lib/detect-changes.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs'),
2 | fsExtra = require('fs-extra'),
3 | path = require('path'),
4 | ignore = require('ignore'),
5 | shelljs = require('shelljs'),
6 | npmlog = require('npmlog'),
7 | sanitize = require('sanitize-filename')
8 |
9 | function makePackageBuilt(lernaPackage, {log = npmlog} = {log: npmlog}) {
10 | return label => {
11 | log.verbose('makePackageBuilt', 'marking module built', {
12 | packagePath: lernaPackage.location,
13 | label
14 | })
15 | fsExtra.ensureDirSync(path.join(process.cwd(), '.lerna'))
16 | fs.writeFileSync(targetFileSentinelFile(lernaPackage, label), '')
17 | }
18 | }
19 |
20 | function makePackageUnbuilt(lernaPackage, {log = npmlog} = {log: npmlog}) {
21 | return label => {
22 | log.verbose('makePackageUnbuilt', 'marking module unbuilt', {
23 | packagePath: lernaPackage.location,
24 | label
25 | })
26 | fsExtra.removeSync(targetFileSentinelFile(lernaPackage, label))
27 | }
28 | }
29 |
30 | function isPackageBuilt(lernaPackage) {
31 | return label => {
32 | const ignored = collectIgnores(lernaPackage.location)
33 | const targetSentinelForPackage = targetFileSentinelFile(lernaPackage, label)
34 | return (
35 | fs.existsSync(targetSentinelForPackage) &&
36 | !modifiedAfter(
37 | lernaPackage.location,
38 | '.',
39 | ignored,
40 | fs.statSync(targetSentinelForPackage).mtime.getTime()
41 | )
42 | )
43 | }
44 | }
45 |
46 | function targetFileSentinelFile(lernaPackage, label = 'default') {
47 | const sanitizedName = sanitize(lernaPackage.name, {replacement: '_'})
48 | return path.resolve(process.cwd(), '.lerna', `.${sanitizedName}-${label}-sentinel`)
49 | }
50 |
51 | function modifiedAfter(baseDir, dir, ignored, timeStamp) {
52 | let rootAbsolutePath = path.resolve(baseDir, dir)
53 | const entries = shelljs.ls(rootAbsolutePath)
54 |
55 | return entries
56 | .map(entry => {
57 | const absolutePath = path.resolve(rootAbsolutePath, entry)
58 | return {
59 | absolutePath,
60 | relativePath: path.relative(baseDir, absolutePath),
61 | stats: fs.lstatSync(absolutePath)
62 | }
63 | })
64 | .filter(({relativePath}) => !ignored.ignores(relativePath))
65 | .filter(({stats}) => !stats.isSymbolicLink())
66 | .sort(({stats}) => (stats.isFile() ? -1 : 1))
67 | .some(({relativePath, stats}) => {
68 | return stats.isDirectory()
69 | ? modifiedAfter(baseDir, relativePath, ignored, timeStamp)
70 | : stats.mtime.getTime() > timeStamp
71 | })
72 | }
73 |
74 | function collectIgnores(dir) {
75 | const paths = [dir]
76 | let current = dir
77 | while (current !== process.cwd()) {
78 | current = current.split(path.sep).slice(0, -1).join(path.sep) || '/'
79 | paths.push(current)
80 | }
81 |
82 | const ig = ignore()
83 | paths.reverse().map(dir => {
84 | if (fs.existsSync(path.join(dir, '.gitignore'))) {
85 | ig.add(fs.readFileSync(path.join(dir, '.gitignore')).toString())
86 | }
87 | })
88 |
89 | return ig
90 | }
91 |
92 | module.exports = {
93 | markPackageBuilt: makePackageBuilt,
94 | markPackageUnbuilt: makePackageUnbuilt,
95 | isPackageBuilt: isPackageBuilt
96 | }
97 |
--------------------------------------------------------------------------------
/tasks/dependencies/test/sync.it.js:
--------------------------------------------------------------------------------
1 | const {aLernaProject, loggerMock, fs} = require('lerna-script-test-utils'),
2 | {expect} = require('chai').use(require('sinon-chai')),
3 | {loadPackages} = require('lerna-script'),
4 | {sync} = require('..')
5 |
6 | describe('sync task', () => {
7 | it('should sync dependencies, depDependencies, peerDependencies defined in root package.json as managed*Dependencies', async () => {
8 | const {log, project} = await setup()
9 |
10 | return project.within(() => {
11 | return sync()(log).then(() => {
12 | expect(fs.readJson('packages/a/package.json')).to.contain.nested.property(
13 | 'peerDependencies.foo',
14 | '> 1.0.0'
15 | )
16 | expect(log.item.info).to.have.been.calledWith(
17 | 'sync',
18 | 'a: peerDependencies.foo (1 -> > 1.0.0)'
19 | )
20 |
21 | expect(fs.readJson('packages/a/package.json')).to.contain.nested.property(
22 | 'devDependencies.lodash',
23 | '1.1.0'
24 | )
25 | expect(log.item.info).to.have.been.calledWith(
26 | 'sync',
27 | 'a: devDependencies.lodash (nope -> 1.1.0)'
28 | )
29 |
30 | expect(fs.readJson('packages/b/package.json')).to.contain.nested.property(
31 | 'dependencies.lodash',
32 | '1.1.0'
33 | )
34 | expect(log.item.info).to.have.been.calledWith(
35 | 'sync',
36 | 'b: dependencies.lodash (~1.0.0 -> 1.1.0)'
37 | )
38 | })
39 | })
40 | })
41 |
42 | it('should use packages if provided', async () => {
43 | const {log, project} = await setup()
44 |
45 | return project.within(async () => {
46 | const packages = await loadPackages()
47 | const filteredPackages = packages.filter(p => p.name === 'a')
48 |
49 | return sync({packages: filteredPackages})(log).then(() => {
50 | expect(fs.readJson('packages/a/package.json')).to.contain.nested.property(
51 | 'peerDependencies.foo',
52 | '> 1.0.0'
53 | )
54 | expect(fs.readJson('packages/a/package.json')).to.contain.nested.property(
55 | 'devDependencies.lodash',
56 | '1.1.0'
57 | )
58 |
59 | expect(fs.readJson('packages/b/package.json')).to.not.contain.nested.property(
60 | 'dependencies.lodash',
61 | '1.1.0'
62 | )
63 | })
64 | })
65 | })
66 |
67 | async function setup() {
68 | const log = loggerMock()
69 | const project = await aLernaProject()
70 | project
71 | .lernaJson({
72 | managedDependencies: {
73 | lodash: '1.1.0'
74 | },
75 | managedPeerDependencies: {
76 | foo: '> 1.0.0'
77 | }
78 | })
79 | .module('packages/a', module =>
80 | module.packageJson({
81 | name: 'a',
82 | version: '1.0.0',
83 | peerDependencies: {
84 | foo: '1'
85 | },
86 | devDependencies: {
87 | lodash: 'nope'
88 | }
89 | })
90 | )
91 | .module('packages/b', module =>
92 | module.packageJson({
93 | name: 'b',
94 | version: '1.0.0',
95 | dependencies: {
96 | a: '~1.0.0',
97 | lodash: '~1.0.0'
98 | }
99 | })
100 | )
101 |
102 | return {log, project}
103 | }
104 | })
105 |
--------------------------------------------------------------------------------
/test-utils/lib/module-builder.js:
--------------------------------------------------------------------------------
1 | const path = require('path'),
2 | {ensureDirSync, removeSync} = require('fs-extra'),
3 | {readFileSync, writeFileSync} = require('fs'),
4 | {execSync} = require('child_process'),
5 | Promise = require('bluebird')
6 |
7 | class ModuleBuilder {
8 | constructor(cwd, dir, isRoot) {
9 | this._isRoot = isRoot || false
10 | this._cwd = cwd
11 | this._dir = dir
12 | this.addFolder(this._dir)
13 | }
14 |
15 | get dir() {
16 | return this._dir
17 | }
18 |
19 | get cwd() {
20 | return this._cwd
21 | }
22 |
23 | get isRoot() {
24 | return this._isRoot
25 | }
26 |
27 | async inDir(fn) {
28 | process.chdir(this.dir)
29 |
30 | try {
31 | await fn(this)
32 | } finally {
33 | process.chdir(this.cwd)
34 | }
35 |
36 | return this
37 | }
38 |
39 | packageJson(overrides = {}) {
40 | return this.addFile('package.json', aPackageJson(this._dir.split('/').pop(), overrides))
41 | }
42 |
43 | lernaJson(overrides = {}) {
44 | return this.addFile('lerna.json', aLernaJson(overrides))
45 | }
46 |
47 | addFile(name, payload) {
48 | this.addFolder(path.dirname(name))
49 |
50 | if (payload && typeof payload !== 'string') {
51 | writeFileSync(path.join(this._dir, name), JSON.stringify(payload, null, 2))
52 | } else {
53 | writeFileSync(path.join(this._dir, name), payload || '')
54 | }
55 |
56 | return this
57 | }
58 |
59 | addFolder(name) {
60 | ensureDirSync(path.resolve(this._dir, name))
61 | return this
62 | }
63 |
64 | module(name, cb) {
65 | const module = new ModuleBuilder(this._cwd, path.join(this._dir, name), false)
66 |
67 | if (cb) {
68 | inDir(cb, module)
69 | } else {
70 | this.inDir(m => m.packageJson(m.dir.split('/').pop()), module)
71 | }
72 | return this
73 | }
74 |
75 | exec(cmd) {
76 | try {
77 | return execSync(cmd).toString()
78 | } catch (e) {
79 | throw new Error(
80 | `Script exited with error code: ${e.status} and output ${e.stdout} + ${e.stderr}`
81 | )
82 | }
83 | }
84 |
85 | readFile(path) {
86 | return readFileSync(path).toString()
87 | }
88 |
89 | readJsonFile(path) {
90 | return JSON.parse(this.readFile(path))
91 | }
92 |
93 | within(fn) {
94 | const clean = () => {
95 | process.chdir(this.cwd)
96 | removeSync(this.dir)
97 | }
98 |
99 | process.chdir(this.dir)
100 |
101 | return Promise.resolve()
102 | .then(() => fn(this))
103 | .finally(clean)
104 | }
105 | }
106 |
107 | function aPackageJson(name, overrides) {
108 | return Object.assign(
109 | {},
110 | {
111 | name: name,
112 | version: '1.0.0',
113 | description: '',
114 | main: 'index.js',
115 | scripts: {
116 | test: 'echo "test script"',
117 | build: 'echo "build script"',
118 | release: 'echo "release script"'
119 | },
120 | author: '',
121 | license: 'ISC'
122 | },
123 | overrides
124 | )
125 | }
126 |
127 | function aLernaJson(overrides) {
128 | return Object.assign(
129 | {},
130 | {
131 | lerna: '2.0.0',
132 | packages: ['packages/**'],
133 | version: '0.0.0'
134 | },
135 | overrides
136 | )
137 | }
138 |
139 | function inDir(fn, module) {
140 | process.chdir(module.dir)
141 |
142 | try {
143 | fn(module)
144 | } finally {
145 | process.chdir(module.cwd)
146 | }
147 |
148 | return module
149 | }
150 |
151 | module.exports = ModuleBuilder
152 |
--------------------------------------------------------------------------------
/lerna-script/lib/iterators.js:
--------------------------------------------------------------------------------
1 | const npmlog = require('npmlog'),
2 | Promise = require('bluebird'),
3 | {markPackageBuilt} = require('./detect-changes'),
4 | {removeBuilt} = require('./filters'),
5 | batchPackages = require('@lerna/batch-packages'),
6 | runParallelBatches = require('@lerna/run-parallel-batches')
7 |
8 | function forEach(lernaPackages, {log = npmlog, build} = {log: npmlog}) {
9 | return taskFn => {
10 | const filteredLernaPackages = filterBuilt(lernaPackages, log, build)
11 | const promisifiedTaskFn = Promise.method(taskFn)
12 | const forEachTracker = log.newItem('forEach', lernaPackages.length)
13 | npmlog.enableProgress()
14 |
15 | return Promise.each(filteredLernaPackages, lernaPackage => {
16 | return promisifiedTaskFn(lernaPackage, forEachTracker)
17 | .then(res => {
18 | build && markPackageBuilt(lernaPackage, {log: forEachTracker})(build)
19 | return res
20 | })
21 | .finally(() => forEachTracker.completeWork(1))
22 | }).finally(() => forEachTracker.finish())
23 | }
24 | }
25 |
26 | function parallel(
27 | lernaPackages,
28 | {log = npmlog, build, concurrency = Infinity} = {log: npmlog, concurrency: Infinity}
29 | ) {
30 | return taskFn => {
31 | const filteredLernaPackages = filterBuilt(lernaPackages, log, build)
32 | const promisifiedTaskFn = Promise.method(taskFn)
33 | const forEachTracker = log.newGroup('parallel', lernaPackages.length)
34 | npmlog.enableProgress()
35 |
36 | return Promise.map(
37 | filteredLernaPackages,
38 | lernaPackage => {
39 | const promiseTracker = forEachTracker.newItem(lernaPackage.name)
40 | promiseTracker.pause()
41 | return promisifiedTaskFn(lernaPackage, promiseTracker)
42 | .then(res => {
43 | build && markPackageBuilt(lernaPackage, {log: forEachTracker})(build)
44 | return res
45 | })
46 | .finally(() => {
47 | promiseTracker.resume()
48 | promiseTracker.completeWork(1)
49 | })
50 | },
51 | {concurrency}
52 | ).finally(() => forEachTracker.finish())
53 | }
54 | }
55 |
56 | function batched(lernaPackages, {log = npmlog, build} = {log: npmlog}) {
57 | return taskFn => {
58 | const filteredLernaPackages = filterBuilt(lernaPackages, log, build)
59 | const promisifiedTaskFn = Promise.method(taskFn)
60 | const forEachTracker = log.newGroup('batched', lernaPackages.length)
61 | npmlog.enableProgress()
62 |
63 | const batchedPackages = batchPackages(filteredLernaPackages, true)
64 | const lernaTaskFn = lernaPackage => {
65 | const promiseTracker = forEachTracker.newItem(lernaPackage.name)
66 | promiseTracker.pause()
67 | return promisifiedTaskFn(lernaPackage, promiseTracker)
68 | .then(() => build && markPackageBuilt(lernaPackage, {log: forEachTracker})(build))
69 | .finally(() => {
70 | promiseTracker.resume()
71 | promiseTracker.completeWork(1)
72 | })
73 | }
74 |
75 | return runParallelBatches(batchedPackages, 4, lernaTaskFn)
76 | }
77 | }
78 |
79 | function filterBuilt(lernaPackages, log, label) {
80 | if (label) {
81 | const filteredLernaPackages = removeBuilt(lernaPackages, {log})(label)
82 | if (filteredLernaPackages.length !== lernaPackages.length) {
83 | log.info(
84 | 'filter',
85 | `filtered-out ${lernaPackages.length - filteredLernaPackages.length} of ${
86 | lernaPackages.length
87 | } built packages`
88 | )
89 | }
90 | return filteredLernaPackages
91 | } else {
92 | return lernaPackages
93 | }
94 | }
95 |
96 | module.exports = {
97 | forEach,
98 | parallel,
99 | batched
100 | }
101 |
--------------------------------------------------------------------------------
/tasks/npmfix/test/npmfix.spec.js:
--------------------------------------------------------------------------------
1 | const {
2 | aLernaProjectWith2Modules,
3 | aLernaProject,
4 | loggerMock,
5 | fs
6 | } = require('lerna-script-test-utils'),
7 | {loadPackages} = require('lerna-script'),
8 | {expect} = require('chai').use(require('sinon-chai')),
9 | npmfix = require('..')
10 |
11 | describe('npmfix task', () => {
12 | describe('should update docs, repo url in package.json', async () => {
13 | const origins = ['https://github.com:git/qwe.git', 'git@github.com:git/qwe.git']
14 | origins.forEach(origin => {
15 | it(`for origin ${origin}`, async () => {
16 | const project = await aLernaProjectWith2Modules()
17 | const log = loggerMock()
18 |
19 | return project.within(ctx => {
20 | ctx.exec(`git remote add origin ${origin}`)
21 | return npmfix()(log).then(() => {
22 | expect(log.info).to.have.been.calledWith(
23 | 'npmfix',
24 | 'fixing homepage, repo urls for 2 packages'
25 | )
26 | expect(fs.readJson('./packages/a/package.json')).to.contain.property(
27 | 'homepage',
28 | 'https://github.com/git/qwe/tree/master/packages/a'
29 | )
30 | expect(fs.readJson('./packages/a/package.json')).to.contain.nested.property(
31 | 'repository.type',
32 | 'git'
33 | )
34 | expect(fs.readJson('./packages/a/package.json')).to.contain.nested.property(
35 | 'repository.url',
36 | 'git@github.com:git/qwe.git'
37 | )
38 | expect(fs.readJson('./packages/a/package.json')).to.contain.nested.property(
39 | 'repository.directory',
40 | '/packages/a'
41 | )
42 |
43 | expect(fs.readJson('./packages/b/package.json')).to.contain.property(
44 | 'homepage',
45 | 'https://github.com/git/qwe/tree/master/packages/b'
46 | )
47 | })
48 | })
49 | })
50 | })
51 | })
52 |
53 | it('should update only for provided modules', async () => {
54 | const project = await aLernaProjectWith2Modules()
55 | const log = loggerMock()
56 |
57 | return project.within(async ctx => {
58 | ctx.exec('git remote add origin git@github.com:git/qwe.git')
59 |
60 | const packages = await loadPackages()
61 | const filteredPackages = packages.filter(p => p.name === 'a')
62 | return npmfix({packages: filteredPackages})(log).then(() => {
63 | expect(fs.readJson('./packages/a/package.json')).to.contain.property(
64 | 'homepage',
65 | 'https://github.com/git/qwe/tree/master/packages/a'
66 | )
67 | expect(fs.readJson('./packages/b/package.json')).to.not.contain.property('homepage')
68 | })
69 | })
70 | })
71 |
72 | it('should sort dependencies', async () => {
73 | function makeDependencies(...names) {
74 | const deps = {}
75 | names.forEach(key => (deps[key] = '1.0'))
76 | return deps
77 | }
78 |
79 | const project = await aLernaProject(
80 | {a: []},
81 | {
82 | dependencies: makeDependencies('z', 'c', 'b'),
83 | devDependencies: makeDependencies('e', 'd'),
84 | peerDependencies: makeDependencies('g', 'f')
85 | }
86 | )
87 |
88 | const log = loggerMock()
89 | return project.within(async ctx => {
90 | ctx.exec('git remote add origin git@github.com:git/qwe.git')
91 |
92 | const packages = await loadPackages()
93 | const filteredPackages = packages.filter(p => p.name === 'a')
94 | return npmfix({packages})(log).then(() => {
95 | const fixedPackage = fs.readJson('./packages/a/package.json')
96 | expect(Object.keys(fixedPackage.dependencies)).to.deep.equal(['b', 'c', 'z'])
97 | expect(Object.keys(fixedPackage.devDependencies)).to.deep.equal(['d', 'e'])
98 | expect(Object.keys(fixedPackage.peerDependencies)).to.deep.equal(['f', 'g'])
99 | })
100 | })
101 | })
102 | })
103 |
--------------------------------------------------------------------------------
/tasks/modules/test/modules.spec.js:
--------------------------------------------------------------------------------
1 | const {aLernaProject, fs, loggerMock} = require('lerna-script-test-utils'),
2 | {loadPackages} = require('lerna-script'),
3 | {expect} = require('chai').use(require('sinon-chai')),
4 | sync = require('..')
5 |
6 | describe('modules sync task', () => {
7 | it('should sync module versions with defaults', async () => {
8 | const log = loggerMock()
9 | const project = await aLernaProject()
10 | project
11 | .module('packages/a', module => module.packageJson({version: '2.0.0'}))
12 | .module('packages/b', module => module.packageJson({dependencies: {a: '~1.0.0'}}))
13 | .module('packages/c', module => module.packageJson({devDependencies: {a: '~1.0.0'}}))
14 | .module('packages/d', module => module.packageJson({peerDependencies: {a: '~1.0.0'}}))
15 |
16 | return project.within(() => {
17 | return sync()(log).then(() => {
18 | expect(log.info).to.have.been.calledWith(
19 | 'modules',
20 | 'syncing module versions for 4 packages'
21 | )
22 | expect(fs.readJson('packages/b/package.json')).to.contain.nested.property(
23 | 'dependencies.a',
24 | '~2.0.0'
25 | )
26 | expect(fs.readJson('packages/c/package.json')).to.contain.nested.property(
27 | 'devDependencies.a',
28 | '~2.0.0'
29 | )
30 | expect(fs.readJson('packages/d/package.json')).to.contain.nested.property(
31 | 'peerDependencies.a',
32 | '>=2.0.0'
33 | )
34 | })
35 | })
36 | })
37 |
38 | it('should respect provided packages', async () => {
39 | const log = loggerMock()
40 | const project = await aLernaProject()
41 | project
42 | .module('packages/a', module => module.packageJson({version: '2.0.0'}))
43 | .module('packages/b', module => module.packageJson({dependencies: {a: '~1.0.0'}}))
44 | .module('packages/c', module => module.packageJson({dependencies: {a: '~1.0.0'}}))
45 |
46 | return project.within(async () => {
47 | const lernaPackages = await loadPackages()
48 | const filteredPackages = lernaPackages.filter(p => p.name !== 'c')
49 | return sync({packages: filteredPackages})(log).then(() => {
50 | expect(log.info).to.have.been.calledWith(
51 | 'modules',
52 | 'syncing module versions for 2 packages'
53 | )
54 | expect(fs.readJson('packages/b/package.json')).to.contain.nested.property(
55 | 'dependencies.a',
56 | '~2.0.0'
57 | )
58 | expect(fs.readJson('packages/c/package.json')).to.contain.nested.property(
59 | 'dependencies.a',
60 | '~1.0.0'
61 | )
62 | })
63 | })
64 | })
65 |
66 | it('should accept custom transformFunctions', async () => {
67 | const log = loggerMock()
68 | const project = await aLernaProject()
69 | project
70 | .module('packages/a', module => module.packageJson({version: '2.0.0'}))
71 | .module('packages/b', module => module.packageJson({dependencies: {a: '~1.0.0'}}))
72 | .module('packages/c', module => module.packageJson({devDependencies: {a: '~1.0.0'}}))
73 | .module('packages/d', module => module.packageJson({peerDependencies: {a: '~1.0.0'}}))
74 |
75 | return project.within(() => {
76 | return sync({transformDependencies: v => `+${v}`, transformPeerDependencies: v => `-${v}`})(
77 | log
78 | ).then(() => {
79 | expect(fs.readJson('packages/b/package.json')).to.contain.nested.property(
80 | 'dependencies.a',
81 | '+2.0.0'
82 | )
83 | expect(fs.readJson('packages/c/package.json')).to.contain.nested.property(
84 | 'devDependencies.a',
85 | '+2.0.0'
86 | )
87 | expect(fs.readJson('packages/d/package.json')).to.contain.nested.property(
88 | 'peerDependencies.a',
89 | '-2.0.0'
90 | )
91 | })
92 | })
93 | })
94 |
95 | it('should beauify json on update', async () => {
96 | const log = loggerMock()
97 | const project = await aLernaProject()
98 | project
99 | .module('packages/a', module => module.packageJson({version: '2.0.0'}))
100 | .module('packages/b', module => module.packageJson({dependencies: {a: '~1.0.0'}}))
101 |
102 | return project.within(() => {
103 | return sync()(log).then(() => {
104 | expect(fs.readFile('packages/b/package.json').split('\n').length).to.be.gt(2)
105 | })
106 | })
107 | })
108 | })
109 |
--------------------------------------------------------------------------------
/lerna-script/lib/filters.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash'),
2 | detectChanges = require('./detect-changes'),
3 | collectUpdates = require('@lerna/collect-updates'),
4 | PackageGraph = require('@lerna/package-graph'),
5 | batchPackages = require('@lerna/batch-packages'),
6 | filterPackages = require('@lerna/filter-packages'),
7 | npmlog = require('npmlog')
8 |
9 | function includeFilteredDeps(allLernaPackages, {log = npmlog} = {log: npmlog}) {
10 | return filteredLernaPackages => {
11 | const packageGraph = new PackageGraph(allLernaPackages)
12 | const withFiltered = packageGraph.addDependencies(filteredLernaPackages)
13 |
14 | const batched = batchPackages(withFiltered)
15 |
16 | return _.flatten(batched)
17 | }
18 | }
19 |
20 | function removeByGlob(lernaPackages, {log = npmlog} = {log: npmlog}) {
21 | return glob => {
22 | const filteredPackages = filterPackages(lernaPackages, [], glob)
23 | const removedPackageNames = diffPackages(lernaPackages, filteredPackages)
24 | log.verbose('removeByGlob', `removed ${removedPackageNames.length} packages`, {
25 | glob,
26 | removed: removedPackageNames
27 | })
28 | return filteredPackages
29 | }
30 | }
31 |
32 | //TODO: see how to make it less sucky
33 | function removeGitSince(lernaPackages, {log = npmlog} = {log: npmlog}) {
34 | return (refspec, opts = {}) => {
35 | const packageGraph = new PackageGraph(lernaPackages)
36 | const collectedPackages = collectUpdates(
37 | lernaPackages,
38 | packageGraph,
39 | {cwd: process.cwd()},
40 | {since: refspec, ...opts}
41 | ).map(graphNode => graphNode.pkg)
42 |
43 | const removedPackageNames = diffPackages(lernaPackages, collectedPackages)
44 | log.verbose('removeGitSince', `removed ${removedPackageNames.length} packages`, {
45 | refspec,
46 | removed: removedPackageNames
47 | })
48 | return collectedPackages
49 | }
50 | }
51 |
52 | function removeBuilt(lernaPackages, {log = npmlog} = {log: npmlog}) {
53 | return label => {
54 | const changedPackages = lernaPackages.filter(
55 | lernaPackage => !detectChanges.isPackageBuilt(lernaPackage)(label)
56 | )
57 | log.verbose('removeBuilt', `found ${changedPackages.length} packages with changes`)
58 | const unbuiltPackages = figureOutAllPackagesThatNeedToBeBuilt(lernaPackages, changedPackages)
59 | unbuiltPackages.forEach(p => detectChanges.markPackageUnbuilt(p)(label))
60 |
61 | const removedPackageNames = diffPackages(lernaPackages, unbuiltPackages)
62 | log.verbose('removeBuilt', `removed ${removedPackageNames.length} packages`, {
63 | label,
64 | removed: removedPackageNames
65 | })
66 |
67 | return unbuiltPackages
68 | }
69 | }
70 |
71 | function figureOutAllPackagesThatNeedToBeBuilt(allPackages, changedPackages) {
72 | const transitiveClosureOfPackagesToBuild = new Set(changedPackages.map(el => el.name))
73 | let dependencyEdges = createDependencyEdgesFromPackages(allPackages)
74 |
75 | let dependencyEdgesLengthBeforeFiltering = dependencyEdges.length
76 | do {
77 | dependencyEdgesLengthBeforeFiltering = dependencyEdges.length
78 |
79 | const newDependencyEdges = []
80 |
81 | for (let edge of dependencyEdges) {
82 | if (transitiveClosureOfPackagesToBuild.has(edge[1])) {
83 | transitiveClosureOfPackagesToBuild.add(edge[0])
84 | } else {
85 | newDependencyEdges.push(edge)
86 | }
87 | }
88 | dependencyEdges = newDependencyEdges
89 | } while (dependencyEdgesLengthBeforeFiltering !== dependencyEdges.length)
90 |
91 | return allPackages.filter(p => transitiveClosureOfPackagesToBuild.has(p.name))
92 | }
93 |
94 | function createDependencyEdgesFromPackages(packages) {
95 | const setOfAllPackageNames = new Set(packages.map(p => p.name))
96 | const packagesByNpmName = _.keyBy(packages, 'name')
97 |
98 | const dependencyEdges = []
99 | packages.forEach(lernaPackage => {
100 | Object.keys({...lernaPackage.dependencies, ...lernaPackage.devDependencies}).forEach(name => {
101 | if (setOfAllPackageNames.has(name)) {
102 | dependencyEdges.push([lernaPackage.name, packagesByNpmName[name].name])
103 | }
104 | })
105 | })
106 |
107 | return dependencyEdges
108 | }
109 |
110 | function diffPackages(before, after) {
111 | return _.difference(
112 | before.map(p => p.name),
113 | after.map(p => p.name)
114 | )
115 | }
116 |
117 | module.exports = {
118 | removeBuilt,
119 | removeGitSince,
120 | includeFilteredDeps,
121 | removeByGlob
122 | }
123 |
--------------------------------------------------------------------------------
/lerna-script/test/cli.spec.js:
--------------------------------------------------------------------------------
1 | const {expect} = require('chai'),
2 | {empty} = require('lerna-script-test-utils'),
3 | {spawnSync} = require('child_process')
4 |
5 | describe('cli', () => {
6 | const runCli = cliRunner()
7 |
8 | it('should fail if lerna.json is missing', done => {
9 | empty()
10 | .within(() => runCli('--loglevel verbose non-existing-task'))
11 | .catch(err => {
12 | expect(err.output).to.match(/.*Resolving lerna-script tasks file.../)
13 | expect(err.output).to.match(/.*no such file or directory, open '\.\/lerna.json'/)
14 | done()
15 | })
16 | })
17 |
18 | it('should fail with error if no tasks file is set and lerna.js is missing', done => {
19 | empty()
20 | .addFile('lerna.json', {})
21 | .within(() => runCli('non-existing-task'))
22 | .catch(err => {
23 | expect(err.output).to.match(/.*Cannot find module.*lerna.js/)
24 | done()
25 | })
26 | })
27 |
28 | it('should fail if task is not provided', done => {
29 | empty()
30 | .within(() => runCli())
31 | .catch(err => {
32 | expect(err.output).to.match(/.*Not enough non-option arguments: got 0, need at least 1/)
33 | done()
34 | })
35 | })
36 |
37 | it('should run exported script from default lerna.js task file', () => {
38 | return empty()
39 | .addFile('lerna.json', {})
40 | .addFile('lerna.js', 'module.exports.someTask = () => console.log("task someTask executed")')
41 | .within(() => runCli('--loglevel verbose someTask'))
42 | .then(res => expect(res.toString()).to.match(/.*task someTask executed/))
43 | })
44 |
45 | it('should run exported script from custom script defined in lerna.json', () => {
46 | return empty()
47 | .addFile('lerna.json', {'lerna-script-tasks': './tasks.js'})
48 | .addFile('tasks.js', 'module.exports.someTask = () => console.log("task someTask executed")')
49 | .within(() => runCli('someTask'))
50 | .then(res => expect(res.toString()).to.match(/.*task someTask executed/))
51 | })
52 |
53 | it('should run exported script from custom script defined in lerna.json as function', () => {
54 | return empty()
55 | .addFile('lerna.json', {'lerna-script-tasks': './tasks.js'})
56 | .addFile(
57 | 'tasks.js',
58 | 'module.exports = () => ({someTask: () => console.log("task someTask executed")})'
59 | )
60 | .within(() => runCli('someTask'))
61 | .then(res => expect(res.toString()).to.match(/.*task someTask executed/))
62 | })
63 |
64 | it('should report error and fail with exit code 1 if task rejected', done => {
65 | empty()
66 | .addFile('lerna.json', {})
67 | .addFile('lerna.js', 'module.exports.someTask = () => Promise.reject(new Error("woop"));')
68 | .within(() => runCli('someTask'))
69 | .catch(e => {
70 | expect(e.status).to.equal(1)
71 | expect(e.output).to.be.string('Task "someTask" failed')
72 | expect(e.output).to.be.string('at Object.module.exports.someTask')
73 | done()
74 | })
75 | })
76 |
77 | it('should set loglevel', () => {
78 | return empty()
79 | .addFile('lerna.json', {})
80 | .addFile('lerna.js', 'module.exports.someTask = log => log.verbose("verbose ok");')
81 | .within(() => runCli('--loglevel verbose someTask'))
82 | .then(output => {
83 | expect(output).to.match(/.*verbose ok/)
84 | })
85 | })
86 |
87 | it('should defaults to info loglevel', () => {
88 | return empty()
89 | .addFile('lerna.json', {})
90 | .addFile('lerna.js', 'module.exports.someTask = log => log.info("info ok");')
91 | .within(() => runCli('--loglevel verbose someTask'))
92 | .then(output => {
93 | expect(output).to.match(/.*info ok/)
94 | })
95 | })
96 |
97 | function cliRunner() {
98 | const cmd = `${process.cwd()}/bin/cli.js`
99 | return (args = '') => {
100 | const res = spawnSync('bash', ['-c', `${cmd} ${args}`], {
101 | cwd: process.cwd(),
102 | stdio: ['pipe', 'pipe', 'pipe']
103 | })
104 | if (res.status !== 0) {
105 | const toThrow = new Error(
106 | `Command failed with status ${res.status} and output ${
107 | res.stdout.toString() + res.stderr.toString()
108 | }`
109 | )
110 | toThrow.output = res.stdout.toString() + res.stderr.toString()
111 | toThrow.status = res.status
112 | throw toThrow
113 | } else {
114 | return res.stdout.toString() + res.stderr.toString()
115 | }
116 | }
117 | }
118 | })
119 |
--------------------------------------------------------------------------------
/lerna-script/test/exec.spec.js:
--------------------------------------------------------------------------------
1 | const {expect} = require('chai').use(require('sinon-chai')),
2 | {captureOutput} = require('./utils'),
3 | {empty, aLernaProjectWith2Modules, loggerMock} = require('lerna-script-test-utils'),
4 | index = require('..'),
5 | invertPromise = require('invert-promise')
6 |
7 | describe('exec', () => {
8 | const output = captureOutput()
9 |
10 | describe('command', () => {
11 | it('should execute command in package cwd and print output by default', async () => {
12 | const log = loggerMock()
13 | const project = await aLernaProjectWith2Modules()
14 |
15 | return project.within(async () => {
16 | const [lernaPackage] = await index.loadPackages()
17 |
18 | return index.exec
19 | .command(lernaPackage, {log})('pwd')
20 | .then(stdout => {
21 | expect(log.silly).to.have.been.calledWith('runCommand', 'pwd', {
22 | cwd: lernaPackage.location,
23 | silent: true
24 | })
25 | expect(stdout).to.equal(lernaPackage.location)
26 | expect(output()).to.not.contain(lernaPackage.location)
27 | })
28 | })
29 | })
30 |
31 | it('should print output if enabled', async () => {
32 | const project = await aLernaProjectWith2Modules()
33 |
34 | return project.within(async () => {
35 | const [lernaPackage] = await index.loadPackages()
36 |
37 | return index.exec
38 | .command(lernaPackage, {silent: false})('ls -lah .')
39 | .then(stdout => {
40 | expect(stdout).to.contain('package.json')
41 | expect(output()).to.contain('package.json')
42 | })
43 | })
44 | })
45 |
46 | it('should reject for a failing command', async () => {
47 | const project = await aLernaProjectWith2Modules()
48 |
49 | return project.within(async () => {
50 | const [lernaPackage] = await index.loadPackages()
51 |
52 | return await invertPromise(index.exec.command(lernaPackage)('asd')).then(e =>
53 | expect(e.message).to.match(/spawn.*ENO/)
54 | )
55 | })
56 | })
57 | })
58 |
59 | describe('script', () => {
60 | it('should execute npm script for package and return output', () => {
61 | const project = empty().addFile('package.json', {
62 | name: 'root',
63 | version: '1.0.0',
64 | scripts: {test: 'echo tested'}
65 | })
66 |
67 | return project.within(async () => {
68 | const lernaPackage = await index.loadRootPackage()
69 |
70 | return index.exec
71 | .script(lernaPackage)('test')
72 | .then(stdout => {
73 | expect(stdout).to.contain('tested')
74 | expect(output()).to.not.contain('tested')
75 | })
76 | })
77 | })
78 |
79 | it('should stream output to stdour/stderr if silent=false', () => {
80 | const project = empty().addFile('package.json', {
81 | name: 'root',
82 | version: '1.0.0',
83 | scripts: {test: 'echo tested'}
84 | })
85 |
86 | return project.within(async () => {
87 | const lernaPackage = await index.loadRootPackage()
88 |
89 | return index.exec
90 | .script(lernaPackage, {silent: false})('test')
91 | .then(stdout => {
92 | expect(stdout).to.contain('tested')
93 | expect(output()).to.contain('tested')
94 | })
95 | })
96 | })
97 |
98 | //TODO: it looks like this one rejects a promise, traced to execa line 210
99 | it('should reject for a failing script', done => {
100 | const project = empty().addFile('package.json', {
101 | name: 'root',
102 | version: '1.0.0',
103 | scripts: {test: 'qwe zzz'}
104 | })
105 |
106 | project.within(async () => {
107 | const lernaPackage = await index.loadRootPackage()
108 |
109 | index.exec
110 | .script(lernaPackage)('test')
111 | .catch(e => {
112 | expect(e.message).to.contain('Command failed: npm run test')
113 | done()
114 | })
115 | })
116 | })
117 |
118 | it('should skip a script and log a warning if its missing', () => {
119 | const log = loggerMock()
120 | const project = empty().addFile('package.json', {name: 'root', version: '1.0.0'})
121 |
122 | return project.within(() => {
123 | const lernaPackage = index.loadRootPackage()
124 |
125 | return index.exec
126 | .script(lernaPackage, {log})('test')
127 | .then(stdout => {
128 | expect(stdout).to.equal('')
129 | expect(log.warn).to.have.been.calledWith('runNpmScript', 'script not found', {
130 | script: 'test',
131 | cwd: lernaPackage.location
132 | })
133 | })
134 | })
135 | })
136 | })
137 | })
138 |
--------------------------------------------------------------------------------
/lerna-script/test/iterators.spec.js:
--------------------------------------------------------------------------------
1 | const {expect} = require('chai'),
2 | {asBuilt, asGitCommited} = require('./utils'),
3 | sinon = require('sinon'),
4 | {aLernaProjectWith2Modules, loggerMock, aLernaProject} = require('lerna-script-test-utils'),
5 | index = require('..'),
6 | Promise = require('bluebird'),
7 | invertPromise = require('invert-promise')
8 |
9 | describe('iterators', () => {
10 | ;['forEach', 'parallel', 'batched'].forEach(type => {
11 | describe(type, () => {
12 | it('should filter-out changed packages', async () => {
13 | const project = await asBuilt(asGitCommited(aLernaProjectWith2Modules()), {label: type})
14 | const log = loggerMock()
15 | let processedPackagesCount = 0
16 |
17 | return project.within(async () => {
18 | const packages = await index.loadPackages()
19 | index.changes.unbuild(packages.find(p => p.name === 'b'))(type)
20 |
21 | return index.iter[type](packages, {build: type, log})(
22 | () => processedPackagesCount++
23 | ).then(() => {
24 | expect(processedPackagesCount).to.equal(1)
25 | expect(log.info).to.have.been.calledWithMatch(
26 | 'filter',
27 | 'filtered-out 1 of 2 built packages'
28 | )
29 | })
30 | })
31 | })
32 |
33 | it('should mark modules as built if "build" is provided', async () => {
34 | const project = await aLernaProjectWith2Modules()
35 | return project.within(async () => {
36 | const packages = await index.loadPackages()
37 |
38 | return index.iter[type](packages, {build: type})(() => Promise.resolve()).then(() => {
39 | packages.forEach(lernaPackage =>
40 | expect(index.changes.isBuilt(lernaPackage)(type)).to.equal(true)
41 | )
42 | })
43 | })
44 | })
45 |
46 | it('should not mark as build on failure', async () => {
47 | const project = await aLernaProjectWith2Modules()
48 | return project.within(async () => {
49 | const packages = await index.loadPackages()
50 |
51 | return invertPromise(
52 | index.iter[type](packages, {build: type})(() => Promise.reject(new Error('woops')))
53 | ).then(() => {
54 | packages.forEach(lernaPackage =>
55 | expect(index.changes.isBuilt(lernaPackage)(type)).to.equal(false)
56 | )
57 | })
58 | })
59 | })
60 | })
61 | })
62 |
63 | describe('forEach', () => {
64 | it('should iterate through available packages', async () => {
65 | const task = sinon.spy()
66 | const log = loggerMock()
67 |
68 | const project = await aLernaProjectWith2Modules()
69 | return project.within(async () => {
70 | const packages = await index.loadPackages()
71 |
72 | return index.iter
73 | .forEach(packages, {log})((pkg, innerLog) => task(pkg.name) || innerLog.info(pkg.name))
74 | .then(() => {
75 | expect(task.getCall(0).args[0]).to.equal('a')
76 | expect(task.getCall(1).args[0]).to.equal('b')
77 |
78 | expect(log.newItem().info).to.have.been.called
79 | })
80 | })
81 | })
82 | })
83 |
84 | describe('parallel', () => {
85 | //TODO: verify async nature?
86 | it('should iterate through available packages', async () => {
87 | const task = sinon.spy()
88 | const log = loggerMock()
89 |
90 | const project = await aLernaProjectWith2Modules()
91 | return project.within(async () => {
92 | const packages = await index.loadPackages()
93 |
94 | return index.iter
95 | .parallel(packages, {log})((pkg, innerLog) => task(pkg.name) || innerLog.info(pkg.name))
96 | .then(() => {
97 | expect(task).to.have.been.calledWith('a')
98 | expect(task).to.have.been.calledWith('b')
99 |
100 | expect(log.newGroup().newItem().info).to.have.been.called
101 | })
102 | })
103 | })
104 |
105 | it('should respect concurrency limit', async () => {
106 | // project with 20 modules
107 | const project = await aLernaProject(
108 | Array.from(Array(20).keys()).reduce((acc, idx) => ({...acc, [`package${idx}`]: []}), {})
109 | )
110 | let concurrentExecutions = 0
111 |
112 | return project.within(async () => {
113 | const packages = await index.loadPackages()
114 | return index.iter.parallel(packages, {concurrency: 3})(async () => {
115 | concurrentExecutions++
116 | expect(concurrentExecutions, 'concurrentExecutions').to.be.at.most(3)
117 | await Promise.delay(5 + Math.random() * 10)
118 | concurrentExecutions--
119 | })
120 | })
121 | })
122 | })
123 |
124 | describe('batched', () => {
125 | //TODO: verify batched nature
126 | it('should iterate through available packages', async () => {
127 | const task = sinon.spy()
128 | const log = loggerMock()
129 | const project = await aLernaProjectWith2Modules()
130 |
131 | return project.within(async () => {
132 | const packages = await index.loadPackages()
133 |
134 | return index.iter
135 | .batched(packages, {log})((pkg, innerLog) => task(pkg.name) || innerLog.info(pkg.name))
136 | .then(() => {
137 | expect(task).to.have.been.calledWith('a')
138 | expect(task).to.have.been.calledWith('b')
139 |
140 | expect(log.newGroup().newItem().info).to.have.been.called
141 | })
142 | })
143 | })
144 | })
145 | })
146 |
--------------------------------------------------------------------------------
/tasks/dependencies/lib/latest.js:
--------------------------------------------------------------------------------
1 | const {exec} = require('child_process'),
2 | {satisfies, validRange, diff} = require('semver'),
3 | inquire = require('./inquire'),
4 | {fs, loadRootPackage} = require('lerna-script'),
5 | os = require('os'),
6 | Promise = require('bluebird')
7 |
8 | function latestDependenciesTask({onInquire = () => ({}), addRange = '', fetch, silent} = {}) {
9 | return log => {
10 | log.info('latest', `checking for latest dependencies`)
11 |
12 | const lernaJson = require(process.cwd() + '/lerna.json')
13 | return checkForLatestDependencies(lernaJson, onInquire, addRange, log, fetch, silent)
14 | }
15 | }
16 |
17 | function checkForLatestDependencies(lernaJson, onInquire, addRange, log, fetch, silent) {
18 | const {
19 | managedDependencies = {},
20 | managedPeerDependencies = {},
21 | autoselect: {versionDiff = [], exclude = []} = {versionDiff: [], exclude: []}
22 | } = lernaJson
23 |
24 | const depsList = Object.keys(cleanLatest(managedDependencies || {}))
25 | const peerDepsList = Object.keys(cleanLatest(managedPeerDependencies || {}))
26 |
27 | const tracker = log.newItem('fetching', depsList.length + peerDepsList.length)
28 |
29 | const concurrency = os.cpus().length
30 |
31 | const depsPromises = Promise.map(
32 | depsList,
33 | depName => fetchLatestVersion(depName, managedDependencies[depName], tracker, fetch),
34 | {concurrency}
35 | )
36 |
37 | const peerDepsPromises = Promise.map(
38 | peerDepsList,
39 | depName => fetchLatestVersion(depName, managedPeerDependencies[depName], tracker, fetch),
40 | {concurrency}
41 | )
42 |
43 | return Promise.all([Promise.all(depsPromises), Promise.all(peerDepsPromises)]).then(
44 | ([deps, peerDeps]) => {
45 | tracker.finish()
46 | log.disableProgress()
47 |
48 | const depsChoices = createChoicesList(deps, 'managedDependencies', versionDiff, exclude)
49 | const peerDepsChoices = createChoicesList(
50 | peerDeps,
51 | 'managedPeerDependencies',
52 | versionDiff,
53 | exclude
54 | )
55 |
56 | if (depsChoices.length + peerDepsChoices.length > 0) {
57 | let selections
58 | if (silent) {
59 | selections = findSelectedUpdates(depsChoices, peerDepsChoices, log)
60 | } else {
61 | selections = inquireSelectedUpdates(depsChoices, peerDepsChoices, onInquire)
62 | }
63 | return selections.then(selectedUpdates => {
64 | return writeSelectedUpdates(selectedUpdates, lernaJson, addRange, log)
65 | })
66 | } else {
67 | log.info('latest', `no updates found, exiting...`)
68 | }
69 | }
70 | )
71 | }
72 |
73 | function cleanLatest(deps) {
74 | Object.keys(deps).forEach(depName => deps[depName] === 'latest' && delete deps[depName])
75 | return deps
76 | }
77 |
78 | function createChoicesList(peerDeps, depType, versionDiff, exclude) {
79 | return peerDeps
80 | .filter(
81 | ({currentVersion, latestVersion}) => !satisfies(latestVersion, validRange(currentVersion))
82 | )
83 | .map(({name, currentVersion, latestVersion}) => {
84 | return {
85 | name: `${name}: ${currentVersion} -> ${latestVersion} (${diff(
86 | currentVersion,
87 | latestVersion
88 | )})`,
89 | value: {type: depType, name, latestVersion},
90 | short: `\n${name}: ${currentVersion} -> ${latestVersion}`,
91 | checked:
92 | versionDiff.includes(diff(currentVersion, latestVersion)) && !exclude.includes(name)
93 | }
94 | })
95 | }
96 |
97 | function inquireSelectedUpdates(depsChoices, peerDepsChoices, onInquire) {
98 | const choiceGroups = []
99 |
100 | if (depsChoices.length > 0) {
101 | choiceGroups.push({name: 'dependencies/devDependencies', choices: depsChoices})
102 | }
103 | if (peerDepsChoices.length > 0) {
104 | choiceGroups.push({name: 'peerDependencies', choices: peerDepsChoices})
105 | }
106 |
107 | onInquire()
108 | return inquire({message: 'Updates found', choiceGroups})
109 | }
110 |
111 | function findSelectedUpdates(depsChoices, peerDepsChoices, log) {
112 | let logs = []
113 |
114 | function getSelectedValues(choices) {
115 | return choices
116 | .filter(c => c.checked)
117 | .map(c => {
118 | logs.push(c.short)
119 | return c.value
120 | })
121 | }
122 |
123 | const selected = getSelectedValues(depsChoices).concat(getSelectedValues(peerDepsChoices))
124 | log.info('latest', logs.join())
125 | return Promise.resolve(selected)
126 | }
127 |
128 | async function writeSelectedUpdates(selectedUpdates, lernaJson, addRange, log) {
129 | if (selectedUpdates.length > 0) {
130 | selectedUpdates.forEach(
131 | ({type, name, latestVersion}) => (lernaJson[type][name] = `${addRange}${latestVersion}`)
132 | )
133 | const rootPackage = await loadRootPackage()
134 | return fs.writeFile(rootPackage)('./lerna.json', lernaJson)
135 | } else {
136 | log.info('latest', `nothing selected, exiting...`)
137 | }
138 | }
139 |
140 | function fetchLatestVersionFromNpm(name) {
141 | return new Promise((resolve, reject) => {
142 | exec(`npm info ${name} dist-tags.latest`, (error, stdout) => {
143 | error ? reject(error) : resolve(stdout.toString().trim('\n'))
144 | })
145 | })
146 | }
147 |
148 | function fetchLatestVersion(name, version, logItem, onFetch) {
149 | const f = onFetch || fetchLatestVersionFromNpm
150 | return f(name, version)
151 | .then(result => ({name, currentVersion: version, latestVersion: result}))
152 | .finally(() => logItem.completeWork(1))
153 | }
154 |
155 | module.exports.task = latestDependenciesTask
156 |
--------------------------------------------------------------------------------
/lerna-script/test/detect-changes.spec.js:
--------------------------------------------------------------------------------
1 | const {expect} = require('chai').use(require('sinon-chai')),
2 | {asBuilt, asGitCommited} = require('./utils'),
3 | {aLernaProjectWith2Modules, loggerMock} = require('lerna-script-test-utils'),
4 | index = require('..'),
5 | {join} = require('path'),
6 | {writeFileSync} = require('fs'),
7 | sinon = require('sinon')
8 |
9 | describe('detect-changes', async () => {
10 | it('should not detect any changes for already marked modules', async () => {
11 | const project = await asBuilt(asGitCommited(aLernaProjectWith2Modules()))
12 |
13 | await project.within(async () => {
14 | const lernaPackages = await index.loadPackages()
15 | lernaPackages.forEach(lernaPackage =>
16 | expect(index.changes.isBuilt(lernaPackage)()).to.equal(true)
17 | )
18 | })
19 | })
20 |
21 | it('should support scoped module names', () => {
22 | expect(() => asBuilt(asGitCommited(aLernaProjectWith2Modules('@foo/a')))).to.not.throw()
23 | })
24 |
25 | it('should detect changes recursively', async () => {
26 | const modules = await aLernaProjectWith2Modules()
27 | const project = await asBuilt(
28 | asGitCommited(modules.inDir(ctx => ctx.addFile('packages/a/test/test.js', '')))
29 | )
30 |
31 | return project.within(async ctx => {
32 | ctx.addFile('packages/a/test/test2.js', '')
33 | const lernaPackages = await index.loadPackages()
34 | const lernaPackage = lernaPackages.find(p => p.name === 'a')
35 |
36 | expect(index.changes.isBuilt(lernaPackage)()).to.equal(false)
37 | })
38 | })
39 |
40 | it('should detect uncommitted modules as changed', async () => {
41 | const project = await aLernaProjectWith2Modules()
42 |
43 | return project.within(async () => {
44 | const lernaPackages = await index.loadPackages()
45 | lernaPackages.forEach(lernaPackage =>
46 | expect(index.changes.isBuilt(lernaPackage)()).to.equal(false)
47 | )
48 | })
49 | })
50 |
51 | it('should detect change in module', async () => {
52 | const project = await asBuilt(asGitCommited(aLernaProjectWith2Modules()))
53 |
54 | return project.within(async () => {
55 | const [aLernaPackage] = await index.loadPackages()
56 | writeFileSync(join(aLernaPackage.location, 'some.txt'), 'qwe')
57 |
58 | expect(index.changes.isBuilt(aLernaPackage)()).to.equal(false)
59 | })
60 | })
61 |
62 | it('should respect .gitignore in root', async () => {
63 | const projectWithGitIgnore = await aLernaProjectWith2Modules()
64 | const project = await asBuilt(
65 | asGitCommited(projectWithGitIgnore.inDir(ctx => ctx.addFile('.gitignore', 'some.txt\n')))
66 | )
67 |
68 | return project.within(async () => {
69 | const [aLernaPackage] = await index.loadPackages()
70 | writeFileSync(join(aLernaPackage.location, 'some.txt'), 'qwe')
71 |
72 | expect(index.changes.isBuilt(aLernaPackage)()).to.equal(true)
73 | })
74 | })
75 |
76 | it('should respect .gitignore in module dir', async () => {
77 | const projectWithGitIgnore = await aLernaProjectWith2Modules()
78 |
79 | const project = await asBuilt(
80 | asGitCommited(
81 | projectWithGitIgnore.inDir(ctx => ctx.addFile('packages/a/.gitignore', 'some.txt\n'))
82 | )
83 | )
84 |
85 | return project.within(async () => {
86 | const packages = await index.loadPackages()
87 | const aLernaPackage = packages.find(lernaPackage => lernaPackage.name === 'a')
88 | writeFileSync(join(aLernaPackage.location, 'some.txt'), 'qwe')
89 |
90 | expect(index.changes.isBuilt(aLernaPackage)()).to.equal(true)
91 | })
92 | })
93 |
94 | it('should unbuild a module', async () => {
95 | const log = loggerMock()
96 | const project = await asBuilt(asGitCommited(aLernaProjectWith2Modules()))
97 |
98 | return project.within(async () => {
99 | const [aLernaPackage] = await index.loadPackages()
100 | index.changes.unbuild(aLernaPackage, {log})()
101 |
102 | expect(index.changes.isBuilt(aLernaPackage)()).to.equal(false)
103 | expect(log.verbose).to.have.been.calledWithMatch(
104 | 'makePackageUnbuilt',
105 | 'marking module unbuilt',
106 | sinon.match.object
107 | )
108 | })
109 | })
110 |
111 | it('should build a module', async () => {
112 | const log = loggerMock()
113 | const project = await asGitCommited(aLernaProjectWith2Modules())
114 |
115 | return project.within(async () => {
116 | const [aLernaPackage] = await index.loadPackages()
117 |
118 | expect(index.changes.isBuilt(aLernaPackage)()).to.equal(false)
119 | index.changes.build(aLernaPackage, {log})()
120 | expect(index.changes.isBuilt(aLernaPackage)()).to.equal(true)
121 | expect(log.verbose).to.have.been.calledWithMatch(
122 | 'makePackageBuilt',
123 | 'marking module built',
124 | sinon.match.object
125 | )
126 | })
127 | })
128 |
129 | it('should respect label for makePackageBuilt', async () => {
130 | const project = await asBuilt(asGitCommited(aLernaProjectWith2Modules()), {label: 'woop'})
131 |
132 | return project.within(async () => {
133 | const lernaPackages = await index.loadPackages()
134 | lernaPackages.forEach(lernaPackage =>
135 | expect(index.changes.isBuilt(lernaPackage)()).to.equal(false)
136 | )
137 | lernaPackages.forEach(lernaPackage =>
138 | expect(index.changes.isBuilt(lernaPackage)('woop')).to.equal(true)
139 | )
140 | })
141 | })
142 |
143 | it('should respect label for makePackageUnbuilt', async () => {
144 | const project = await asBuilt(asGitCommited(aLernaProjectWith2Modules()), {label: 'woop'})
145 |
146 | return project.within(async () => {
147 | const [aLernaPackage] = await index.loadPackages()
148 | index.changes.unbuild(aLernaPackage)()
149 | expect(index.changes.isBuilt(aLernaPackage)('woop')).to.equal(true)
150 |
151 | index.changes.unbuild(aLernaPackage)('woop')
152 | expect(index.changes.isBuilt(aLernaPackage)()).to.equal(false)
153 | })
154 | })
155 | })
156 |
--------------------------------------------------------------------------------
/tasks/idea/index.js:
--------------------------------------------------------------------------------
1 | const {join, relative} = require('path'),
2 | templates = require('./lib/templates'),
3 | shelljs = require('shelljs'),
4 | {loadRootPackage, loadPackages, exec, iter} = require('lerna-script'),
5 | {execSync} = require('child_process')
6 |
7 | const EXCLUDE_FOLDERS = ['node_modules', 'dist']
8 | const LANGIAGE_LEVEL = 'ES6'
9 |
10 | const SUPPORTED_SOURCE_LEVELS = [
11 | {name: 'test', isTestSource: true},
12 | {name: 'tests', isTestSource: true},
13 | {name: 'src', isTestSource: false},
14 | {name: 'lib', isTestSource: false},
15 | {name: 'scripts', isTestSource: false}
16 | ]
17 |
18 | const DEFAULT_MOCHA_CONFIGURATIONS = packageJson => {
19 | return [
20 | {
21 | name: packageJson.name,
22 | environmentVariables: {
23 | DEBUG: 'wix:*'
24 | },
25 | extraOptions: '--exit',
26 | testKind: 'PATTERN',
27 | testPattern: 'test/**/*.spec.js test/**/*.it.js test/**/*.e2e.js'
28 | }
29 | ]
30 | }
31 |
32 | //TODO: add options: {packages, mocha: {patterns: ''}}
33 | function generateIdeaProject({packages, mochaConfigurations, excludePatterns, addRoot} = {}) {
34 | const mochaConfigurationsFn = mochaConfigurations || DEFAULT_MOCHA_CONFIGURATIONS
35 | return async log => {
36 | const rootPackage = await loadRootPackage()
37 | const lernaPackages = await (packages || loadPackages())
38 | const execInRoot = cmd => {
39 | log.verbose('idea', `executing command: ${cmd}`)
40 | return exec.command(rootPackage)(cmd)
41 | }
42 |
43 | log.info('idea', `Generating idea projects for ${lernaPackages.length} packages`)
44 | log.info('idea', `cleaning existing project files...`)
45 | return execInRoot('rm -rf .idea')
46 | .then(() => execInRoot('rm -f *.iml'))
47 | .then(() => execInRoot('mkdir .idea'))
48 | .then(() => log.info('idea', 'writing project files'))
49 | .then(() =>
50 | execInRoot(
51 | `cp ${join(__dirname, '/files/vcs.xml')} ${join(rootPackage.location, '.idea/')}`
52 | )
53 | )
54 | .then(() => {
55 | createWorkspaceXml(lernaPackages, rootPackage, mochaConfigurationsFn, log)
56 | createModulesXml(lernaPackages, rootPackage, log, addRoot)
57 |
58 | log.info('idea', 'writing module files')
59 |
60 | if (addRoot) {
61 | log.info('idea', 'writing root module file')
62 | templates.ideaRootModuleImlFile(join(rootPackage.location, 'root.iml'))
63 | }
64 |
65 | return iter.parallel(lernaPackages, {log})((lernaPackage, log) => {
66 | return exec
67 | .command(lernaPackage)('rm -f *.iml')
68 | .then(() => createModuleIml(lernaPackage, log, excludePatterns))
69 | })
70 | })
71 | }
72 | }
73 |
74 | function createWorkspaceXml(lernaPackages, rootPackage, mochaConfigurations, log) {
75 | log.verbose('idea', 'creating .idea/workspace.xml')
76 | const nodePath = execSync('which node').toString().replace('\n', '')
77 | const mochaPackage = resolveMochaPackage(rootPackage, lernaPackages, log)
78 |
79 | log.verbose('idea', `setting node - using current system node: '${nodePath}'`)
80 | log.verbose('idea', `setting language level to: '${LANGIAGE_LEVEL}'`)
81 | log.verbose('idea', `setting mocha package: '${mochaPackage}'`)
82 | const config = {
83 | modules: lernaPackages.map(lernaPackage => ({
84 | name: lernaPackage.name,
85 | relativePath: relative(rootPackage.location, lernaPackage.location),
86 | nodePath,
87 | mocha: mochaConfigurations(lernaPackage.toJSON())
88 | })),
89 | mochaPackage,
90 | languageLevel: LANGIAGE_LEVEL
91 | }
92 |
93 | templates.ideaWorkspaceXmlFile(join(rootPackage.location, '.idea', 'workspace.xml'), config)
94 | }
95 |
96 | function createModulesXml(lernaPackages, rootPackage, log, addRoot) {
97 | log.verbose('idea', 'creating .idea/modules.xml')
98 | templates.ideaModulesFile(
99 | join(rootPackage.location, '.idea', 'modules.xml'),
100 | lernaPackages.map(lernaPackage => {
101 | const relativePath = relative(rootPackage.location, lernaPackage.location)
102 | const name = stripScope(lernaPackage.name)
103 | if (relativePath.indexOf('/') > -1) {
104 | const parts = relativePath.split('/')
105 | parts.pop()
106 | return {name, dir: relativePath, group: parts.join('/')}
107 | } else {
108 | return {name, dir: relativePath}
109 | }
110 | }),
111 | {addRoot}
112 | )
113 | }
114 |
115 | function createModuleIml(lernaPackage, log, excludePatterns) {
116 | const directories = shelljs
117 | .ls(lernaPackage.location)
118 | .filter(entry => shelljs.test('-d', join(lernaPackage.location, entry)))
119 |
120 | const sourceFolders = []
121 | SUPPORTED_SOURCE_LEVELS.forEach(sourceFolder => {
122 | if (directories.indexOf(sourceFolder.name) > -1) {
123 | sourceFolders.push(sourceFolder)
124 | }
125 | })
126 |
127 | log.verbose('idea', `writing module: '${lernaPackage.name}'`, {
128 | sourceFolders,
129 | excludeFolders: EXCLUDE_FOLDERS
130 | })
131 | const imlFile = join(lernaPackage.location, stripScope(lernaPackage.name) + '.iml')
132 | templates.ideaModuleImlFile(imlFile, {
133 | excludeFolders: EXCLUDE_FOLDERS,
134 | sourceFolders,
135 | excludePatterns
136 | })
137 | }
138 |
139 | function stripScope(name) {
140 | const sep = name.indexOf('/')
141 | return sep === -1 ? name : name.substring(sep + 1)
142 | }
143 |
144 | function resolveMochaPackage(rootPackage, lernaPackages, log) {
145 | let mochaLocation
146 |
147 | if (shelljs.test('-d', join(rootPackage.location, 'node_modules/mocha'))) {
148 | log.info(
149 | 'idea',
150 | `using mocha package from: '${join(rootPackage.location, 'node_modules/mocha')}'`
151 | )
152 | mochaLocation = 'node_modules/mocha'
153 | } else {
154 | lernaPackages.some(pkg => {
155 | if (shelljs.test('-d', join(pkg.location, 'node_modules/mocha'))) {
156 | log.info('idea', `using mocha package from: '${join(pkg.location, 'node_modules/mocha')}'`)
157 | mochaLocation = join(relative(rootPackage.location, pkg.location), 'node_modules', 'mocha')
158 | return true
159 | }
160 | })
161 | }
162 |
163 | return (
164 | mochaLocation ||
165 | join(relative(rootPackage.location, lernaPackages[0].location), 'node_modules', 'mocha')
166 | )
167 | }
168 |
169 | module.exports = generateIdeaProject
170 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # lerna-script [](https://travis-ci.org/wix/lerna-script) [](https://lernajs.io/) [](https://github.com/prettier/prettier)
2 |
3 | [Lerna](https://lernajs.io/) is a nice tool to manage JavaScript projects with multiple packages, but sometimes you need
4 | more than it provides. [lerna-script](https://www.npmjs.com/package/lerna-script) might be just the thing you need. It allows
5 | you to add custom tasks/scripts to automate multiple package management routine tasks. Some use cases:
6 |
7 | - normalize `package.json`s of all modules (ex. fix repo url, docs url) on pre-push/pre-commit;
8 | - generate [WebStorm](https://www.jetbrains.com/webstorm/) project for all modules in repo;
9 | - sync dependencies across all modules - ex. to have same version of mocha;
10 | - have composite tasks (install, run npm scripts) to ease maintenance for large teams/OSS projects.
11 | - regenerate readme's for root readme and all modules that are using ex. [markdown-magic](https://github.com/DavidWells/markdown-magic);
12 | - whatever else you need.
13 |
14 | # Install
15 |
16 | ```bash
17 | npm install --save-dev lerna-script
18 | ```
19 |
20 | # Usage
21 |
22 | - [Basic usage example](#basic-usage-example)
23 | - [Incremental builds](#incremental-builds)
24 | - [Tasks](#tasks)
25 | - [Git hooks](#git-hooks)
26 | - [External presets](#external-presets)
27 |
28 | ## Basic usage example
29 |
30 | Add `lerna-script` launcher to `package.json` scripts:
31 |
32 | ```json
33 | {
34 | "scripts": {
35 | "ls": "lerna-script"
36 | }
37 | }
38 | ```
39 |
40 | To start using, add `lerna.js` to root of your mono-repo and add initial task:
41 |
42 | ```js
43 | const {loadPackages, iter, exec} = require('lerna-script'),
44 | {join} = require('path');
45 |
46 | async function syncNvmRc(log) {
47 | log.info('syncNvmRc', 'syncing .nvmrc to all modules from root');
48 | const packages = await loadPackages();
49 |
50 | return iter.parallel(packages)(lernaPackage => {
51 | exec.command(lernaPackage)(`cp ${join(process.cwd(), '.nvmrc')} .`);
52 | });
53 | }
54 |
55 | module.exports.syncNvmRc = syncNvmRc;
56 | ```
57 |
58 | And then you can run it:
59 |
60 | ```bash
61 | npm run ls syncNvmRc
62 | ```
63 |
64 | What happened here:
65 |
66 | - you created `lerna.js` where each export is a task referenced by export name you can execute via `lerna-script [export]`;
67 | - you used functions from `lerna-script` which are just thin wrappers around [lerna api](https://github.com/lerna/lerna/tree/master/src);
68 | - you created task to sync root `.nvmrc` to all modules so that all of them have same node version defined.
69 |
70 | You could also fallback to [lerna api](https://github.com/lerna/lerna/tree/master/src) and write same task as:
71 |
72 | ```js
73 | const Repository = require('lerna/lib/Repository'),
74 | PackageUtilities = require('lerna/lib/PackageUtilities'),
75 | {join} = require('path'),
76 | {execSync} = require('child_process');
77 |
78 | module.exports.syncNvmRc = () => {
79 | const rootNvmRcPath = join(process.cwd(), '.nvmrc');
80 |
81 | return PackageUtilities.getPackages(new Repository()).forEach(lernaPackage => {
82 | execSync(`cp ${rootNvmRcPath}`, {cwd: lernaPackage.location});
83 | });
84 | };
85 | ```
86 |
87 | To see available function please check-out [lerna-script](./lerna-script), for pre-cooked tasks check-out [tasks](./tasks).
88 |
89 | ## Incremental builds
90 |
91 | [Lerna](https://lernajs.io/) provides a way to run commands (bootstrap, npm scripts, exec) either for all modules or a sub-tree based on git
92 | diff from a ref (master, tag, commit), but does not provide a way to run actions incrementally. One use case would be to
93 | run tests for all modules, once one of the modules fail, fix it an continue, so you don't have to rerun tests for modules
94 | that already passed. Or do a change and run tests for a subtree that might be impacted by a change given module dependency
95 | graph.
96 |
97 | For this [lerna-script](./lerna-script) provides means to both mark modules as built and filter-out already built modules:
98 |
99 | ```js
100 | const {loadPackages, iter, exec, changes, filters} = require('lerna-script');
101 |
102 | module.exports.test = log => {
103 | return iter.forEach(changedPackages, {log, build: 'test'})(lernaPackage => {
104 | return exec.script(lernaPackage)('test');
105 | });
106 | };
107 | ```
108 |
109 | where property `build` on `forEach` marks processed package as built with label `test`. For different tasks you can have separate labels so they do not clash.
110 |
111 | ## Tasks
112 |
113 | [lerna-script](.) has some pre-assembled tasks/task-sets for solving some problem. Examples:
114 |
115 | - [idea](./tasks/idea) - to generate [WebStorm](https://www.jetbrains.com/webstorm/) project for all modules in repo;
116 | - [npmfix](./tasks/npmfix) - to fix repo, docs links for all modules matching their git path;
117 | - [modules](./tasks/modules) - to align module versions across repo;
118 | - [depcheck](./tasks/depcheck) - to run [depcheck](https://github.com/depcheck/depcheck) for all modules in repo;
119 | - [dependencies](./tasks/dependencies) - group of tasks to manage/sync/update dependency versions for all modules.
120 |
121 | ## Git hooks
122 |
123 | Sometimes there are things you want to make sure are done/enforced on your modules like:
124 |
125 | - linting all modules in repo;
126 | - making sure some meta is normalized automatically across all modules;
127 | - ...
128 |
129 | Recommendation is to combine [lerna-script](https://www.npmjs.com/package/lerna-script) with [husky](https://www.npmjs.com/package/husky) for running automatic actions on pre-push/pre-commit hooks. Then you don't have to think about it and it just happens automatically.
130 |
131 | Say you want to make sure that [repository](https://docs.npmjs.com/files/package.json#repository) url is valid for all modules and you don't leave it out when adding new module (via amazing copy/paste pattern).
132 |
133 | For that you could add a [lerna-script](https://www.npmjs.com/package/lerna-script) task to normalize [repository](https://docs.npmjs.com/files/package.json#repository) and hook-it up to [pre-push git hook](https://git-scm.com/book/gr/v2/Customizing-Git-Git-Hooks).
134 |
135 | First install husky:
136 |
137 | ```bash
138 | npm install --save-dev husky
139 | ```
140 |
141 | then add script to `package.json`
142 |
143 | ```json
144 | {
145 | "scripts": {
146 | "prepush": "lerna-script update-repo-urls"
147 | }
148 | }
149 | ```
150 |
151 | and add export to `lerna.js`:
152 |
153 | ```js
154 | const npmfix = require('lerna-script-tasks-npmfix');
155 |
156 | module.exports['update-repo-urls'] = npmfix();
157 | ```
158 |
159 | ## External presets
160 |
161 | You can also use presets or otherwise tasks exprted by external modules. `lerna-script` by default reads tasks from `lerna.js`,
162 | but you can actually write tasks in any other file(module) and define it in your `lerna.json` like:
163 |
164 | ```json
165 | {
166 | "lerna-script-tasks": "./tasks.js"
167 | }
168 | ```
169 |
--------------------------------------------------------------------------------
/tasks/dependencies/test/latest.it.js:
--------------------------------------------------------------------------------
1 | const {aLernaProject, loggerMock} = require('lerna-script-test-utils'),
2 | {expect} = require('chai').use(require('sinon-chai')),
3 | {latest} = require('..'),
4 | {execSync} = require('child_process'),
5 | bddStdin = require('bdd-stdin'),
6 | Promise = require('bluebird')
7 |
8 | describe('latest task', function () {
9 | this.timeout(30000)
10 |
11 | it('should list dependencies that can be updated', async () => {
12 | const ramdaVersion = execSync('npm info ramda dist-tags.latest').toString().trim('\n')
13 | const lodashVersion = execSync('npm info lodash dist-tags.latest').toString().trim('\n')
14 | const {log, project} = await setup({
15 | managedDependencies: {
16 | lodash: 'latest',
17 | shelljs: '*',
18 | ramda: '0.0.1',
19 | url: '>0.0.1'
20 | },
21 | managedPeerDependencies: {
22 | ramda: '> 0.0.1',
23 | lodash: '0.0.1'
24 | }
25 | })
26 |
27 | const onInquire = () => bddStdin('a', '\n')
28 |
29 | return project.within(ctx => {
30 | return latest({onInquire})(log).then(() => {
31 | const lernaJson = ctx.readJsonFile('lerna.json')
32 |
33 | expect(lernaJson.managedPeerDependencies.lodash).to.equal(lodashVersion)
34 | expect(lernaJson.managedDependencies.ramda).to.equal(ramdaVersion)
35 | expect(lernaJson.managedPeerDependencies.ramda).to.not.equal(ramdaVersion)
36 | })
37 | })
38 | })
39 |
40 | it('should respect range operator when provided', async () => {
41 | const ramdaVersion = execSync('npm info ramda dist-tags.latest').toString().trim('\n')
42 | const lodashVersion = execSync('npm info lodash dist-tags.latest').toString().trim('\n')
43 | const {log, project} = await setup({
44 | managedDependencies: {
45 | lodash: 'latest',
46 | shelljs: '*',
47 | ramda: '0.0.1',
48 | url: '>0.0.1'
49 | },
50 | managedPeerDependencies: {
51 | ramda: '> 0.0.1',
52 | lodash: '0.0.1'
53 | }
54 | })
55 |
56 | const onInquire = () => bddStdin('a', '\n')
57 |
58 | return project.within(ctx => {
59 | return latest({onInquire, addRange: '+'})(log).then(() => {
60 | const lernaJson = ctx.readJsonFile('lerna.json')
61 |
62 | expect(lernaJson.managedPeerDependencies.lodash).to.equal(`+${lodashVersion}`)
63 | expect(lernaJson.managedDependencies.ramda).to.equal(`+${ramdaVersion}`)
64 | expect(lernaJson.managedPeerDependencies.ramda).to.not.equal(ramdaVersion)
65 | })
66 | })
67 | })
68 |
69 | it('should log and exit for no updates', async () => {
70 | const {log, project} = await setup({
71 | managedDependencies: {
72 | lodash: 'latest'
73 | }
74 | })
75 |
76 | return project.within(() => {
77 | return latest()(log).then(() => {
78 | expect(log.info).to.have.been.calledWith('latest', `no updates found, exiting...`)
79 | })
80 | })
81 | })
82 |
83 | it('should not reject for missing managedDependencies, managedPeerDependencies', async () => {
84 | const {log, project} = await setup()
85 |
86 | return project.within(() => latest()(log))
87 | })
88 |
89 | describe('auto select', () => {
90 | it('should autoselect minor and patch updates', async () => {
91 | const {log, project} = await setup({
92 | managedDependencies: {
93 | package1: '1.0.0'
94 | },
95 | managedPeerDependencies: {
96 | package2: '2.0.0',
97 | package3: '3.0.0'
98 | },
99 | autoselect: {
100 | versionDiff: ['patch', 'minor']
101 | }
102 | })
103 |
104 | const onInquire = () => bddStdin('\n')
105 | const fetch = name => {
106 | switch (name) {
107 | case 'package1':
108 | return Promise.resolve('1.0.1') //patch diff
109 | case 'package2':
110 | return Promise.resolve('2.1.1') //minor diff
111 | case 'package3':
112 | return Promise.resolve('4.1.1') //major diff
113 | }
114 | }
115 |
116 | return project.within(ctx => {
117 | return latest({onInquire, fetch})(log).then(() => {
118 | const lernaJson = ctx.readJsonFile('lerna.json')
119 |
120 | expect(lernaJson.managedDependencies.package1).to.equal('1.0.1')
121 | expect(lernaJson.managedPeerDependencies.package2).to.equal('2.1.1')
122 | expect(lernaJson.managedPeerDependencies.package3).to.equal('3.0.0')
123 | })
124 | })
125 | })
126 |
127 | it('should autoselect major updates but exclude speicific packages', async () => {
128 | const {log, project} = await setup({
129 | managedDependencies: {
130 | package1: '1.0.0'
131 | },
132 | managedPeerDependencies: {
133 | package2: '2.0.0',
134 | package3: '3.0.0'
135 | },
136 | autoselect: {
137 | versionDiff: ['major'],
138 | exclude: ['package3']
139 | }
140 | })
141 |
142 | const onInquire = () => bddStdin('\n')
143 | const fetch = name => {
144 | switch (name) {
145 | case 'package1':
146 | return Promise.resolve('4.1.1') //major
147 | case 'package2':
148 | return Promise.resolve('2.1.2') //minor
149 | case 'package3':
150 | return Promise.resolve('4.0.9') //major
151 | }
152 | }
153 |
154 | return project.within(ctx => {
155 | return latest({onInquire, fetch})(log).then(() => {
156 | const lernaJson = ctx.readJsonFile('lerna.json')
157 |
158 | expect(lernaJson.managedDependencies.package1).to.equal('4.1.1')
159 | expect(lernaJson.managedPeerDependencies.package2).to.equal('2.0.0')
160 | expect(lernaJson.managedPeerDependencies.package3).to.equal('3.0.0')
161 | })
162 | })
163 | })
164 |
165 | it('should respect silent flag', async () => {
166 | const {log, project} = await setup({
167 | managedDependencies: {
168 | package1: '1.0.0'
169 | },
170 | managedPeerDependencies: {
171 | package2: '2.0.0',
172 | package3: '3.0.0'
173 | },
174 | autoselect: {
175 | versionDiff: ['patch'],
176 | exclude: ['package1', 'package3']
177 | }
178 | })
179 |
180 | const fetch = name => {
181 | switch (name) {
182 | case 'package1':
183 | return Promise.resolve('1.0.1') //patch
184 | case 'package2':
185 | return Promise.resolve('2.0.1') //patch
186 | case 'package3':
187 | return Promise.resolve('3.0.9') //patch
188 | }
189 | }
190 |
191 | return project.within(ctx => {
192 | return latest({fetch, silent: true})(log).then(() => {
193 | const lernaJson = ctx.readJsonFile('lerna.json')
194 |
195 | expect(lernaJson.managedDependencies.package1).to.equal('1.0.0')
196 | expect(lernaJson.managedPeerDependencies.package2).to.equal('2.0.1')
197 | expect(lernaJson.managedPeerDependencies.package3).to.equal('3.0.0')
198 | })
199 | })
200 | })
201 | })
202 |
203 | async function setup(lernaJsonOverrides = {}) {
204 | const log = loggerMock()
205 | const project = await aLernaProject()
206 | project.lernaJson(lernaJsonOverrides)
207 |
208 | return {log, project}
209 | }
210 | })
211 |
--------------------------------------------------------------------------------
/lerna-script/README.md:
--------------------------------------------------------------------------------
1 | # lerna-script
2 |
3 | For usage scenarios documentation please see [root of repo](../README.md);
4 |
5 | # CLI
6 |
7 | `lerna-script` exports a cli script:
8 |
9 | ```bash
10 | lerna-script [options]
11 | ```
12 |
13 | where options:
14 |
15 | - loglevel - set's loglevel, defaults to `info`;
16 |
17 | task:
18 |
19 | - one of exports defined in `lerna.js` file.
20 |
21 | # API
22 |
23 | ### loadPackages({[log], [packageConfigs]}): Promise[LernaPackages[]]
24 |
25 | Returns list of packages/modules in repo - forward to lerna;
26 |
27 | Parameters:
28 |
29 | - log, optional - `npmlog` logger;
30 |
31 | ### loadRootPackage({[log]}): Promise[LernaPackage[]]
32 |
33 | Returns [Package](https://github.com/lerna/lerna/blob/master/src/Package.js) of root module.
34 |
35 | Parameters:
36 |
37 | - log, optional - `npmlog` logger;
38 |
39 | ### iter.forEach(lernaPackages, {[log], [build]})(task): Promise
40 |
41 | Executed provided command for all `lernaPackages` in a serial fashion. `taskFn` can be either sync task or return a `Promise`.
42 |
43 | Parameters:
44 |
45 | - lernaPackages - list of lerna packages to iterate on;
46 | - log - logger to be used for progress and pass-on to nested tasks;
47 | - build - should a module be built as in `changes.build`;
48 | - task - function to execute with signature `(lernaPackage, log) => Promise`.
49 |
50 | Returns promise with task results.
51 |
52 | ### iter.parallel(lernaPackages, {[log], [build], [concurrency]})(task): Promise
53 |
54 | Executed provided command for all `lernaPackages` in a parallel fashion(`Promise.all`). `taskFn` can be either sync task
55 | or return a `Promise`.
56 |
57 | Parameters:
58 |
59 | - lernaPackages - list of lerna packages to iterate on;
60 | - log - logger to be used for progress and pass-on to nested tasks;
61 | - build - should a module be built as in `changes.build`;
62 | - task - function to execute with signature `(lernaPackage, log) => Promise`.
63 | - concurrency - number, defaults to `Infinity`. See [bluebird#map API](http://bluebirdjs.com/docs/api/promise.map.html#map-option-concurrency)
64 |
65 | Returns promise with task results.
66 |
67 | ### iter.batched(lernaPackages, {[log], [build]})(task): Promise
68 |
69 | Executed provided command for all `lernaPackages` in a batched fashion respecting dependency graph. `taskFn` can be either
70 | sync task or return a `Promise`.
71 |
72 | Parameters:
73 |
74 | - lernaPackages - list of lerna packages to iterate on;
75 | - log - logger to be used for progress and pass-on to nested tasks;
76 | - build - should a module be built as in `changes.build`;
77 | - task - function to execute with signature `(lernaPackage, log) => Promise`.
78 |
79 | Returns promise without results (undefined).
80 |
81 | ### exec.command(lernaPackage, {silent = true})(command): Promise(stdout)
82 |
83 | Executes given command for a package and returns collected `stdout`.
84 |
85 | Note that `command` is a single command, meaning `rm -f zzz` and not ex. `rm -f zzz && mkdir zzz`. It's just for convenience
86 | you can provide command and args as a single string.
87 |
88 | Argument list #1:
89 |
90 | - command - command to execute;
91 |
92 | Argument list #2:
93 |
94 | - lernaPackage - package returned either by `rootPackage()` or `packages()`;
95 | - silent - should command output be streamed to stdout/stderr or suppressed. Defaults to `true`;
96 |
97 | Returns:
98 |
99 | - stdout - collected output;
100 |
101 | ### exec.script(lernaPackage, {silent = true})(script): Promise(stdout)
102 |
103 | Executes given npm script for a package and returns collected `stdout`.
104 |
105 | Argument list #1:
106 |
107 | - script - npm script to execute;
108 |
109 | Argument list #2:
110 |
111 | - lernaPackage - package returned either by `rootPackage()` or `packages()`;
112 | - silent - should script output be streamed to stdout/stderr or suppressed. Defaults to `true`;
113 |
114 | Returns:
115 |
116 | - stdout - collected output;
117 |
118 | ### changes.build(lernaPackage, {[log]})([label]): undefined
119 |
120 | Marks package as built.
121 |
122 | Parameters:
123 |
124 | - lernaPackage - package to build;
125 | - log, optional - `npmlog` logger;
126 | - label, optional - given you have several exports scripts, you can separate them in different build/unbuild groups by label.
127 |
128 | ### changes.unbuild(lernaPackage, {[log]})([label]): undefined
129 |
130 | Marks package as unbuilt.
131 |
132 | Parameters:
133 |
134 | - lernaPackage - package to unbuild;
135 | - log, optional - `npmlog` logger;
136 | - label, optional - given you have several exports scripts, you can separate them in different build/unbuild groups by label
137 |
138 | ### changes.isBuilt(lernaPackage)([label]): boolean
139 |
140 | Returns true if package is build and false otherwise.
141 |
142 | Parameters:
143 |
144 | - lernaPackage - package to unbuild;
145 | - label, optional - given you have several exports scripts, you can separate them in different build/unbuild groups by label
146 |
147 | ### filters.removeBuilt(lernaPackages: [], {[log]})([label]: String): []
148 |
149 | Filters-out packages that have been marked as built `changes.build` and were not changed since. Note that it filters-out also dependent packages, so if:
150 |
151 | - a, did not change, depends on b;
152 | - b, changed;
153 | - c, not changed, no inter-project dependencies.
154 |
155 | Then it will return only `c` as `b` has changed and `a` depends on `b`, so it needs to be rebuilt/retested/re...
156 |
157 | Parameters:
158 |
159 | - lernaPackages - packages to filter;
160 | - log, optional - `npmlog` logger;
161 | - label, optional - given you have several exports scripts, you can separate them in different build/unbuild groups by label
162 |
163 | **Note:** this filter mutates built/unbuild state, meaning that it unbuilds dependents to get reproducible runs.
164 |
165 | ### filters.gitSince(lernaPackages: [], {[log]})(refspec: String, {ignoreChanges: string[]}?): []
166 |
167 | Filters-out packages that have did not change since `refspec` - ex. master, brach, tag.
168 |
169 | Parameters:
170 |
171 | - lernaPackages - packages to filter;
172 | - log, optional - `npmlog` logger;
173 | - refspec - git `refspec` = master, branchname, tag...
174 | - opts.ignoreChanges - optional array of glob expressions. files matching those globs will be ignored in the diff calculation.
175 |
176 | ### filters.removeByGlob(lernaPackages: [], {[log]})(glob: String): []
177 |
178 | Filters-out packages by provided glob pattern.
179 |
180 | Parameters:
181 |
182 | - lernaPackages - packages to filter;
183 | - log, optional - `npmlog` logger;
184 | - glob - glob pattern.
185 |
186 | ### filters.includeFilteredDeps(lernaPackages: [], {[log]})(filteredPackages: []): []
187 |
188 | Returns a list of packages tgat includes dependencies of `filteredPackages` that are in `lernaPackages`.
189 |
190 | Parameters:
191 |
192 | - lernaPackages - all packages;
193 | - log, optional - `npmlog` logger;
194 | - filteredPackages - subset of `lernaPackages`.
195 |
196 | ### fs.readFile(lernaPackage)(relativePath, converter: buffer => ?): Promise[?]
197 |
198 | Reads a file as string by default or accepts a custom converter.
199 |
200 | Parameters:
201 |
202 | - lernaPackage - a lerna package for cwd of reading;
203 | - relativePath - file path relative to `lernaPackage` root.
204 | - converter - a function to convert content, ex. `JSON.parse`
205 |
206 | ### fs.writeFile(lernaPackage)(relativePath, content, converter: type => string): Promise[String]
207 |
208 | Writes string/buffer to file, accepts custom formatter.
209 |
210 | Automatically detects and formats object.
211 |
212 | Parameters:
213 |
214 | - lernaPackage - a lerna package for cwd of reading;
215 | - relativePath - file path relative to `lernaPackage` root.
216 | - content - content of file.
217 | - converter - function to convert provided type to string/buffer.
218 |
--------------------------------------------------------------------------------
/lerna-script/test/filters.spec.js:
--------------------------------------------------------------------------------
1 | const {expect} = require('chai').use(require('sinon-chai')),
2 | {asBuilt, asGitCommited} = require('./utils'),
3 | Package = require('@lerna/package'),
4 | {empty, aLernaProjectWith2Modules, loggerMock} = require('lerna-script-test-utils'),
5 | index = require('..')
6 |
7 | describe('filters', function () {
8 | this.timeout(5000)
9 |
10 | describe('includeFilteredDeps', () => {
11 | it('should add dependent package', async () => {
12 | const log = loggerMock()
13 | const project = await aLernaProjectWith2Modules()
14 |
15 | return project.within(async () => {
16 | const allPackages = await index.loadPackages()
17 | const lernaPackages = index.filters.removeByGlob(allPackages, {log})('a')
18 | const filteredPackages = index.filters.includeFilteredDeps(allPackages, {log})(
19 | lernaPackages
20 | )
21 | expect(filteredPackages.map(p => p.name)).to.deep.equal(['a', 'b'])
22 | })
23 | })
24 | })
25 |
26 | describe('removeByGlob', () => {
27 | it('should filter-out packages by provided glob', async () => {
28 | const log = loggerMock()
29 | const project = await aLernaProjectWith2Modules()
30 |
31 | return project.within(async () => {
32 | const packages = await index.loadPackages()
33 | const lernaPackages = index.filters.removeByGlob(packages, {log})('a')
34 | expect(lernaPackages.map(p => p.name)).to.have.same.members(['b'])
35 | expect(log.verbose).to.have.been.calledWithMatch('removeByGlob', 'removed 1 packages')
36 | })
37 | })
38 | })
39 |
40 | describe('removeBuilt', () => {
41 | it('should not filter-out any packages for unbuilt project', async () => {
42 | const project = await aLernaProjectWith2Modules()
43 |
44 | return project.within(async () => {
45 | const packages = await index.loadPackages()
46 | const unbuiltLernaPackages = index.filters.removeBuilt(packages)()
47 | expect(unbuiltLernaPackages.length).to.equal(2)
48 | })
49 | })
50 |
51 | it('should filter-out changed packages', async () => {
52 | const log = loggerMock()
53 | const project = await asBuilt(asGitCommited(aLernaProjectWith2Modules()))
54 |
55 | return project.within(async () => {
56 | const packages = await index.loadPackages()
57 | index.changes.unbuild(packages.find(p => p.name === 'b'))()
58 |
59 | const unbuiltLernaPackages = index.filters.removeBuilt(packages, {log})()
60 |
61 | expect(unbuiltLernaPackages.length).to.equal(1)
62 | expect(log.verbose).to.have.been.calledWithMatch(
63 | 'removeBuilt',
64 | 'found 1 packages with changes'
65 | )
66 | expect(log.verbose).to.have.been.calledWithMatch('removeBuilt', 'removed 1 packages')
67 | })
68 | })
69 |
70 | it('should filter-out packages whose dependencies changed', async () => {
71 | const project = await asBuilt(asGitCommited(aLernaProjectWith2Modules()))
72 |
73 | return project.within(async () => {
74 | const lernaPackages = await index.loadPackages()
75 | index.changes.unbuild(lernaPackages.find(lernaPackage => lernaPackage.name === 'b'))()
76 |
77 | const unbuiltLernaPackages = index.filters.removeBuilt(lernaPackages)()
78 | expect(unbuiltLernaPackages.length).to.equal(1)
79 | })
80 | })
81 |
82 | it('should respect labels when filtering-out packages', async () => {
83 | const project = await asBuilt(asGitCommited(aLernaProjectWith2Modules()), {label: 'woop'})
84 |
85 | return project.within(async () => {
86 | const lernaPackages = await index.loadPackages()
87 |
88 | index.changes.unbuild(lernaPackages.find(lernaPackage => lernaPackage.name === 'b'))()
89 | expect(index.filters.removeBuilt(lernaPackages)('woop').length).to.equal(0)
90 |
91 | index.changes.unbuild(lernaPackages.find(lernaPackage => lernaPackage.name === 'b'))('woop')
92 | expect(index.filters.removeBuilt(lernaPackages)('woop').length).to.equal(1)
93 | })
94 | })
95 |
96 | it('should unmark dependents as built', async () => {
97 | const project = await asBuilt(asGitCommited(aLernaProjectWith2Modules()))
98 |
99 | return project.within(async ctx => {
100 | const lernaPackages = await index.loadPackages()
101 | index.changes.unbuild(lernaPackages.find(lernaPackage => lernaPackage.name === 'a'))()
102 |
103 | expect(index.filters.removeBuilt(lernaPackages)().length).to.equal(2)
104 |
105 | index.changes.build(lernaPackages.find(lernaPackage => lernaPackage.name === 'a'))()
106 | ctx.exec('sleep 1')
107 | expect(index.filters.removeBuilt(lernaPackages)().length).to.equal(1)
108 | })
109 | })
110 | })
111 |
112 | describe('filters.gitSince', () => {
113 | it('removes modules without changes', async () => {
114 | const log = loggerMock()
115 | const project = empty()
116 | .addFile('package.json', {name: 'root', version: '1.0.0'})
117 | .addFile('lerna.json', {lerna: '2.0.0', packages: ['packages/**'], version: '0.0.0'})
118 | .module('packages/a', module => module.packageJson({name: 'a', version: '2.0.0'}))
119 | .module('packages/ba', module =>
120 | module.packageJson({name: 'ba', version: '1.0.0', dependencies: {b: '~1.0.0'}})
121 | )
122 |
123 | await project.inDir(ctx => {
124 | ctx.exec('git init && git config user.email mail@example.org && git config user.name name')
125 | ctx.exec('git add -A && git commit -am ok')
126 | ctx.exec('git checkout -b test')
127 | })
128 |
129 | project.module('packages/b', module => module.packageJson({name: 'b', version: '1.0.0'}))
130 |
131 | await project.inDir(ctx => {
132 | ctx.exec('git add -A && git commit -am ok')
133 | })
134 | return project.within(async () => {
135 | const packages = await index.loadPackages()
136 | const lernaPackages = index.filters.gitSince(packages, {log})('master')
137 |
138 | expect(lernaPackages[0]).to.be.instanceof(Package)
139 | expect(lernaPackages.map(p => p.name)).to.have.same.members(['b', 'ba'])
140 | expect(log.verbose).to.have.been.calledWithMatch('removeGitSince', 'removed 1 packages')
141 | })
142 | })
143 |
144 | it('respects ignore list', async () => {
145 | const log = loggerMock()
146 | const project = empty()
147 | .addFile('package.json', {name: 'root', version: '1.0.0'})
148 | .addFile('lerna.json', {lerna: '2.0.0', packages: ['packages/**'], version: '0.0.0'})
149 | .module('packages/a', module =>
150 | module
151 | .packageJson({name: 'a', version: '2.0.0'})
152 | .addFile('pom.xml', '1')
153 | )
154 | .module('packages/b', module =>
155 | module
156 | .packageJson({name: 'b', version: '1.0.0'})
157 | .addFile('pom.xml', '1')
158 | )
159 |
160 | await project.inDir(ctx => {
161 | ctx.exec('git init && git config user.email mail@example.org && git config user.name name')
162 | ctx.exec('git add -A && git commit -am ok')
163 | ctx.exec('git checkout -b test')
164 | })
165 |
166 | project.module('packages/b', module => module.addFile('pom.xml', '2'))
167 |
168 | await project.inDir(ctx => {
169 | ctx.exec('git add -A && git commit -am ok')
170 | })
171 |
172 | return project.within(async () => {
173 | const packages = await index.loadPackages()
174 | const lernaPackages = index.filters.gitSince(packages, {log})('master', {
175 | ignoreChanges: ['pom.xml']
176 | })
177 | expect(lernaPackages).to.be.empty
178 | })
179 | })
180 | })
181 | })
182 |
--------------------------------------------------------------------------------
/tasks/idea/test/idea.spec.js:
--------------------------------------------------------------------------------
1 | const {loggerMock, aLernaProject, aLernaProjectWith2Modules} = require('lerna-script-test-utils'),
2 | {loadPackages} = require('lerna-script'),
3 | {expect} = require('chai').use(require('sinon-chai')),
4 | idea = require('..'),
5 | shelljs = require('shelljs')
6 |
7 | describe('idea', async () => {
8 | it('should generate idea project files', async () => {
9 | const log = loggerMock()
10 | const project = await aLernaProjectWith3Modules()
11 | return project.within(() => {
12 | return idea()(log).then(() => assertIdeaFilesGenerated())
13 | })
14 | })
15 |
16 | it('should generate idea project files for provided modules', async () => {
17 | const log = loggerMock()
18 | const project = await aLernaProjectWith3Modules()
19 |
20 | return project.within(async () => {
21 | const packages = await loadPackages()
22 | const filteredPackages = packages.filter(p => p.name === 'a')
23 | return idea({packages: filteredPackages})(log).then(() => {
24 | expect(shelljs.test('-f', 'packages/a/a.iml')).to.equal(true)
25 | expect(shelljs.test('-f', 'packages/b/b.iml')).to.equal(false)
26 | expect(shelljs.test('-f', 'packages/c/c.iml')).to.equal(false)
27 | })
28 | })
29 | })
30 |
31 | it('should set language level to ES6', async () => {
32 | const log = loggerMock()
33 | const project = await aLernaProjectWith3Modules()
34 |
35 | return project.within(() => {
36 | return idea()(log).then(() => {
37 | expect(shelljs.cat('.idea/workspace.xml').stdout).to.be.string(
38 | ''
39 | )
40 | })
41 | })
42 | })
43 |
44 | it.skip('removes existing .idea project files before generating new ones', async () => {
45 | const log = loggerMock()
46 |
47 | const project = await aLernaProjectWith3Modules()
48 | project.addFile('.idea/some-existing-file.txt', 'qwe')
49 |
50 | return project.within(() => {
51 | return idea()(log).then(() => {
52 | expect(shelljs.test('-f', '.idea/some-existing-file.txt')).to.equal(false)
53 | })
54 | })
55 | })
56 |
57 | it('generates [module-name].iml with node_modules, dist excluded so idea would not index all deps', async () => {
58 | const log = loggerMock()
59 | const project = await aLernaProjectWith3Modules()
60 |
61 | return project.within(() => {
62 | return idea()(log).then(() => {
63 | expect(shelljs.cat('packages/a/a.iml').stdout).to.be.string(
64 | ''
65 | )
66 | expect(shelljs.cat('packages/a/a.iml').stdout).to.be.string(
67 | ''
68 | )
69 | })
70 | })
71 | })
72 |
73 | it('generates [module-name].iml with pattern exclusions', async () => {
74 | const log = loggerMock()
75 | const project = await aLernaProjectWith3Modules()
76 |
77 | return project.within(() => {
78 | return idea({excludePatterns: ['somePattern.*', 'anotherPattern.*']})(log).then(() => {
79 | expect(shelljs.cat('packages/a/a.iml').stdout).to.be.string(
80 | ''
81 | )
82 | expect(shelljs.cat('packages/a/a.iml').stdout).to.be.string(
83 | ''
84 | )
85 | })
86 | })
87 | })
88 |
89 | it('generates [module-name].iml and marks test/tests as test root', async () => {
90 | const log = loggerMock()
91 | const project = await aLernaProjectWith3Modules()
92 |
93 | project.addFolder('packages/a/test').addFolder('packages/a/tests')
94 |
95 | return project.within(() => {
96 | return idea()(log).then(() => {
97 | const imlFile = shelljs.cat('packages/a/a.iml').stdout
98 | expect(imlFile).to.be.string(
99 | ''
100 | )
101 | expect(imlFile).to.be.string(
102 | ''
103 | )
104 | })
105 | })
106 | })
107 |
108 | it('generates [module-name].iml and marks src/lib/scripts as source roots', async () => {
109 | const log = loggerMock()
110 | const project = await aLernaProjectWith3Modules()
111 | project.addFolder('packages/a/src').addFolder('packages/a/lib').addFolder('packages/a/scripts')
112 |
113 | return project.within(() => {
114 | return idea()(log).then(() => {
115 | const imlFile = shelljs.cat('packages/a/a.iml').stdout
116 | expect(imlFile).to.be.string(
117 | ''
118 | )
119 | expect(imlFile).to.be.string(
120 | ''
121 | )
122 | expect(imlFile).to.be.string(
123 | ''
124 | )
125 | })
126 | })
127 | })
128 |
129 | it('does not generate root.iml by default', async () => {
130 | const log = loggerMock()
131 | const project = await aLernaProjectWith3Modules()
132 |
133 | return project.within(() => {
134 | return idea()(log).then(() => {
135 | expect(shelljs.test('-e', './root.iml')).to.be.false
136 | expect(shelljs.cat('.idea/modules.xml').stdout).to.not.be.string('root.iml')
137 | })
138 | })
139 | })
140 |
141 | it('generates root.iml when configured to do so via "addRoot" flag', async () => {
142 | const log = loggerMock()
143 | const project = await aLernaProjectWith3Modules()
144 |
145 | return project.within(() => {
146 | return idea({addRoot: true})(log).then(() => {
147 | expect(shelljs.test('-e', './root.iml')).to.be.true
148 | expect(shelljs.cat('.idea/modules.xml').stdout).to.be.string('root.iml')
149 | })
150 | })
151 | })
152 |
153 | context('mocha configurations', async () => {
154 | it('generates Mocha run configurations for all modules with mocha, extra options, interpreter and env set', async () => {
155 | const log = loggerMock()
156 | const project = await aLernaProjectWith3Modules()
157 | return project.within(() => {
158 | return idea()(log).then(() => {
159 | const node = shelljs.exec('which node').stdout.replace('\n', '')
160 |
161 | expect(shelljs.cat('.idea/workspace.xml').stdout).to.be.string(
162 | `$PROJECT_DIR$/packages/a/node_modules/mocha`
163 | )
164 | expect(shelljs.cat('.idea/workspace.xml').stdout).to.be.string(
165 | ``
166 | )
167 | expect(shelljs.cat('.idea/workspace.xml').stdout).to.be.string(
168 | ''
169 | )
170 | expect(shelljs.cat('.idea/workspace.xml').stdout).to.be.string(
171 | ''
172 | )
173 | expect(shelljs.cat('.idea/workspace.xml').stdout).to.be.string(
174 | 'PATTERN'
175 | )
176 | expect(shelljs.cat('.idea/workspace.xml').stdout).to.be.string(
177 | 'test/**/*.spec.js test/**/*.it.js test/**/*.e2e.js'
178 | )
179 | expect(shelljs.cat('.idea/workspace.xml').stdout).to.be.string(
180 | `${node}`
181 | )
182 | expect(shelljs.cat('.idea/workspace.xml').stdout).to.be.string(
183 | `$PROJECT_DIR$/packages/a`
184 | )
185 | expect(shelljs.cat('.idea/workspace.xml').stdout).to.be.string(
186 | '--exit'
187 | )
188 | })
189 | })
190 | })
191 |
192 | it('respects custom mocha config', async () => {
193 | const log = loggerMock()
194 | const mochaConfig = packageJson => [
195 | {
196 | name: packageJson.name + 'custom',
197 | environmentVariables: {
198 | NODEBUG: 'woop',
199 | GUBEDON: 'poow'
200 | },
201 | extraOptions: 'woo-extra',
202 | testKind: 'PATTERN_woo',
203 | testPattern: 'test-pattern-woo'
204 | }
205 | ]
206 |
207 | const project = await aLernaProject({a: []})
208 |
209 | return project.within(() => {
210 | return idea({mochaConfigurations: mochaConfig})(log).then(() => {
211 | const node = shelljs.exec('which node').stdout.replace('\n', '')
212 | expect(shelljs.cat('.idea/workspace.xml').stdout).to.be.string(
213 | '$PROJECT_DIR$/packages/a/node_modules/mocha'
214 | )
215 | expect(shelljs.cat('.idea/workspace.xml').stdout).to.be.string(
216 | ''
217 | )
218 | expect(shelljs.cat('.idea/workspace.xml').stdout).to.match(
219 | /\s*\s*\s*<\/envs>/g
220 | )
221 | expect(shelljs.cat('.idea/workspace.xml').stdout).to.be.string(
222 | 'PATTERN_woo'
223 | )
224 | expect(shelljs.cat('.idea/workspace.xml').stdout).to.be.string(
225 | 'test-pattern-woo'
226 | )
227 | expect(shelljs.cat('.idea/workspace.xml').stdout).to.be.string(
228 | `${node}`
229 | )
230 | expect(shelljs.cat('.idea/workspace.xml').stdout).to.be.string(
231 | `$PROJECT_DIR$/packages/a`
232 | )
233 | expect(shelljs.cat('.idea/workspace.xml').stdout).to.be.string(
234 | 'woo-extra'
235 | )
236 | })
237 | })
238 | })
239 |
240 | it('does generate multiple mocha configs per module', async () => {
241 | const log = loggerMock()
242 | const mochaConfig = packageJson => [
243 | {name: packageJson.name + 'custom1'},
244 | {name: packageJson.name + 'custom2'}
245 | ]
246 |
247 | const project = await aLernaProject({a: []})
248 |
249 | return project.within(() => {
250 | return idea({mochaConfigurations: mochaConfig})(log).then(() => {
251 | expect(shelljs.cat('.idea/workspace.xml').stdout).to.be.string(
252 | ''
253 | )
254 | expect(shelljs.cat('.idea/workspace.xml').stdout).to.be.string(
255 | ''
256 | )
257 | })
258 | })
259 | })
260 |
261 | it('does not generate mocha configuration if empty list is provided', async () => {
262 | const log = loggerMock()
263 | const project = await aLernaProject({a: []})
264 |
265 | return project.within(() => {
266 | return idea({mochaConfigurations: () => []})(log).then(() => {
267 | expect(shelljs.cat('.idea/workspace.xml').stdout).to.not.be.string(
268 | ''
269 | )
270 | })
271 | })
272 | })
273 |
274 | context('mocha module resolution', async () => {
275 | it('uses mocha from root node_modules if mocha package is present', async () => {
276 | const log = loggerMock()
277 | const project = await aLernaProject({a: []})
278 |
279 | return project.within(ctx => {
280 | ctx.addFolder('node_modules/mocha')
281 |
282 | return idea()(log).then(() => {
283 | expect(shelljs.cat('.idea/workspace.xml').stdout).to.be.string(
284 | '$PROJECT_DIR$/node_modules/mocha'
285 | )
286 | })
287 | })
288 | })
289 |
290 | it('uses mocha from one of modules if present', async () => {
291 | const log = loggerMock()
292 | const project = await aLernaProject({a: [], b: []})
293 |
294 | return project.within(ctx => {
295 | ctx.addFolder('packages/b/node_modules/mocha')
296 |
297 | return idea()(log).then(() => {
298 | expect(shelljs.cat('.idea/workspace.xml').stdout).to.be.string(
299 | '$PROJECT_DIR$/packages/b/node_modules/mocha'
300 | )
301 | })
302 | })
303 | })
304 | })
305 | })
306 |
307 | it('adds modules to groups if they are in subfolders', async () => {
308 | const log = loggerMock()
309 | const project = await aLernaProjectWith3Modules()
310 |
311 | return project.within(() => {
312 | return idea()(log).then(() => {
313 | const modulesXml = shelljs.cat('.idea/modules.xml').stdout
314 |
315 | expect(modulesXml).to.be.string('group="packages"')
316 | expect(modulesXml).to.not.be.string('group="a"')
317 | expect(modulesXml).to.not.be.string('group="b"')
318 | })
319 | })
320 | })
321 |
322 | it('creates git-based ./idea/vcs.xml', async () => {
323 | const log = loggerMock()
324 | const project = await aLernaProjectWith3Modules()
325 | return project.within(() => {
326 | return idea()(log).then(() => {
327 | expect(shelljs.cat('.idea/vcs.xml').stdout).to.be.string(
328 | ''
329 | )
330 | })
331 | })
332 | })
333 |
334 | function aLernaProjectWith3Modules() {
335 | return aLernaProject({a: [], b: ['a'], c: ['a', '@wix/d'], '@wix/d': ['a']})
336 | }
337 |
338 | function assertIdeaFilesGenerated() {
339 | expect(shelljs.test('-d', '.idea')).to.equal(true)
340 | expect(shelljs.test('-f', '.idea/workspace.xml')).to.equal(true)
341 | expect(shelljs.test('-f', '.idea/vcs.xml')).to.equal(true)
342 | expect(shelljs.test('-f', '.idea/modules.xml')).to.equal(true)
343 | expect(shelljs.test('-f', 'packages/a/a.iml')).to.equal(true)
344 | expect(shelljs.test('-f', 'packages/b/b.iml')).to.equal(true)
345 | expect(shelljs.test('-f', 'packages/c/c.iml')).to.equal(true)
346 | expect(shelljs.test('-f', 'packages/d/d.iml')).to.equal(true)
347 | }
348 | })
349 |
--------------------------------------------------------------------------------