├── .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 [![Build Status](https://img.shields.io/travis/wix/lerna-script/master.svg?label=build%20status)](https://travis-ci.org/wix/lerna-script) [![lerna](https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg)](https://lernajs.io/) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](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 | --------------------------------------------------------------------------------