diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 489d0964..b11a07ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,5 +52,9 @@ jobs: run: npm run ci env: NODE_OPTIONS: --max_old_space_size=6144 + - name: Code Coverage + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index ea81f80f..ba3b1da1 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,7 @@ test/fixtures/flatten/package.json test/fixtures/forbidden-license/package.json test/fixtures/initial-cnpmrc/package.json test/fixtures/local/package.json +test/fixtures/npm-workspaces/packages/c/ +test/fixtures/npm-workspaces/package.json + +package-lock.json diff --git a/README.md b/README.md index 7488f2b0..4f2ca2d5 100644 --- a/README.md +++ b/README.md @@ -81,9 +81,6 @@ Options: --cache-strict: use disk cache even on production env ``` - -[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fcnpm%2Fnpminstall.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fcnpm%2Fnpminstall?ref=badge_large) - #### npmuninstall ```bash @@ -167,6 +164,7 @@ const npminstall = require('npminstall'); - [x] uninstall - [x] resolutions - [x] [npm alias](https://github.com/npm/rfcs/blob/latest/implemented/0001-package-aliases.md) +- [x] [npm workspaces](https://docs.npmjs.com/cli/v9/using-npm/workspaces?v=true) ## Different with NPM diff --git a/bin/install.js b/bin/install.js index a61d5d4d..87216298 100755 --- a/bin/install.js +++ b/bin/install.js @@ -35,6 +35,9 @@ Object.assign(argv, parseArgs(originalArgv, { 'tarball-url-mapping', 'proxy', 'dependencies-tree', + // npminstall foo --workspace=aa + // npminstall foo -w aa + 'workspace', ], boolean: [ 'version', @@ -67,12 +70,13 @@ Object.assign(argv, parseArgs(originalArgv, { 'disable-dedupe', 'save-dependencies-tree', 'force-link-latest', + 'workspaces', ], default: { optional: true, }, alias: { - // npm install [-S|--save|-D|--save-dev|-O|--save-optional] [-E|--save-exact] [-d|--detail] + // npm install [-S|--save|-D|--save-dev|-O|--save-optional] [-E|--save-exact] [-d|--detail] [-w|--workspace] S: 'save', D: 'save-dev', O: 'save-optional', @@ -83,6 +87,7 @@ Object.assign(argv, parseArgs(originalArgv, { c: 'china', r: 'registry', d: 'detail', + w: 'workspace', }, }) ); @@ -98,6 +103,9 @@ Usage: npminstall npminstall + npminstall --workspace= + npminstall -w + npminstall --workspaces npminstall @ npminstall @ npminstall @ @@ -121,6 +129,8 @@ Options: -r, --registry: specify custom registry -c, --china: specify in china, will automatically using chinese npm registry and other binary's mirrors -d, --detail: show detail log of installation + -w, --workspace: install on one workspace only, e.g.: npminstall koa -w a + --workspaces: install new package on all workspaces, e.g: npminstall foo --workspaces --trace: show memory and cpu usages traces of installation --ignore-scripts: ignore all preinstall / install and postinstall scripts during the installation --no-optional: ignore all optionalDependencies during the installation @@ -167,6 +177,8 @@ if (Array.isArray(root)) { // use last one, e.g.: $ npminstall --root=abc --root=def root = root[root.length - 1]; } +let installOnAllWorkspaces = argv.workspaces; +const installWorkspaceName = !installOnAllWorkspaces && argv.workspace; const production = argv.production || process.env.NODE_ENV === 'production'; let cacheDir = argv.cache === false ? '' : null; if (production) { @@ -228,6 +240,28 @@ for (const key in argv) { debug('argv: %j, env: %j', argv, env); (async () => { + const { workspaceRoots, workspacesMap } = await utils.readWorkspaces(root); + if (workspacesMap.size > 0) { + for (const info of workspacesMap.values()) { + // link to root/node_modules + const linkDir = path.join(root, 'node_modules', info.package.name); + await utils.forceSymlink(info.root, linkDir); + debug('add workspace %s on %s', info.package.name, info.root); + } + } + // ignore --workspaces if there is no any workspace + if (installOnAllWorkspaces && workspacesMap.size === 0) { + installOnAllWorkspaces = false; + } + + if (installWorkspaceName) { + const installWorkspaceInfo = await utils.getWorkspaceInfo(root, installWorkspaceName, workspacesMap); + if (!installWorkspaceInfo) { + throw new Error(`No workspaces found: --workspace=${installWorkspaceName}`); + } + root = installWorkspaceInfo.root; + } + let binaryMirrors = {}; if (inChina) { @@ -264,6 +298,7 @@ debug('argv: %j, env: %j', argv, env); proxy, prune, disableDedupe: argv['disable-dedupe'], + workspacesMap, }; config.strictSSL = getStrictSSL(); config.ignoreScripts = argv['ignore-scripts'] || getIgnoreScripts(); @@ -385,7 +420,31 @@ debug('argv: %j, env: %j', argv, env); } } } - await installLocal(config, context); + // install workspaces first + // https://docs.npmjs.com/cli/v9/using-npm/workspaces?v=true + if (!installWorkspaceName && pkgs.length === 0 && workspaceRoots.length > 0) { + // install in workspaces + for (const workspaceRoot of workspaceRoots) { + const workspaceConfig = { + ...config, + root: workspaceRoot, + }; + await installLocal(workspaceConfig); + } + } + // install packages on all workspaces + if (installOnAllWorkspaces && pkgs.length > 0) { + for (const workspaceRoot of workspaceRoots) { + const workspaceConfig = { + ...config, + root: workspaceRoot, + }; + await installLocal(workspaceConfig); + } + } else { + await installLocal(config, context); + } + if (pkgs.length > 0) { // support --save, --save-dev, --save-optional, --save-client, --save-build and --save-isomorphic const map = { @@ -397,15 +456,19 @@ debug('argv: %j, env: %j', argv, env); 'save-isomorphic': 'isomorphicDependencies', }; - // install saves any specified packages into dependencies by default. - if (Object.keys(map).every(key => !argv[key]) && !argv['no-save']) { - await updateDependencies(root, pkgs, map.save, argv['save-exact'], config.remoteNames); - } else { - for (const key in map) { - if (argv[key]) await updateDependencies(root, pkgs, map[key], argv['save-exact'], config.remoteNames); + // install saves any specified packages into dependencies by default. + const saveRootDirs = installOnAllWorkspaces ? workspaceRoots : [ root ]; + for (const saveRootDir of saveRootDirs) { + if (Object.keys(map).every(key => !argv[key]) && !argv['no-save']) { + await updateDependencies(saveRootDir, pkgs, map.save, argv['save-exact'], config.remoteNames); + } else { + for (const key in map) { + if (argv[key]) { + await updateDependencies(saveRootDir, pkgs, map[key], argv['save-exact'], config.remoteNames); + } + } } } - } } @@ -464,18 +527,26 @@ async function updateDependencies(root, pkgs, propName, saveExact, remoteNames) } else if (item.type === ALIAS_TYPES) { deps[item.name] = item.version; } else { - const pkgDir = LOCAL_TYPES.includes(item.type) ? item.version : path.join(root, 'node_modules', item.name); - const itemPkg = await utils.readJSON(path.join(pkgDir, 'package.json')); - + let saveName; + let saveVersion; + if (item.workspacePackage) { + saveName = item.workspacePackage.name; + saveVersion = item.workspacePackage.version || item.version; + } else { + const pkgDir = LOCAL_TYPES.includes(item.type) ? item.version : path.join(root, 'node_modules', item.name); + const itemPkg = await utils.readJSON(path.join(pkgDir, 'package.json')); + saveName = itemPkg.name; + saveVersion = itemPkg.version; + } let saveSpec; // If install with `cnpm i foo`, the type is tag but rawSpec is empty string if (item.arg.type === 'tag' && item.arg.rawSpec) { saveSpec = item.arg.rawSpec; } else { const savePrefix = saveExact ? '' : getVersionSavePrefix(); - saveSpec = `${savePrefix}${itemPkg.version}`; + saveSpec = `${savePrefix}${saveVersion}`; } - deps[itemPkg.name] = saveSpec; + deps[saveName] = saveSpec; } } // sort pkg[propName] diff --git a/bin/update.js b/bin/update.js index 07a8c510..6f078121 100755 --- a/bin/update.js +++ b/bin/update.js @@ -2,7 +2,7 @@ const path = require('path'); const parseArgs = require('minimist'); -const { rimraf } = require('../lib/utils'); +const { rimraf, readWorkspaces, getWorkspaceInfo } = require('../lib/utils'); function help(root) { console.log(` @@ -18,20 +18,37 @@ Usage: const argv = parseArgs(process.argv.slice(2), { string: [ 'root', + 'workspace', ], boolean: [ 'help', ], alias: { h: 'help', + w: 'workspace', }, }); - const root = argv.root || process.cwd(); + let root = argv.root || process.cwd(); if (argv.help) return help(root); - const nodeModules = path.join(root, 'node_modules'); - console.log('[npmupdate] removing %s', nodeModules); - await rimraf(nodeModules); + const installWorkspaceName = argv.workspace; + const { workspaceRoots, workspacesMap } = await readWorkspaces(root); + let roots = []; + if (installWorkspaceName) { + const installWorkspaceInfo = await getWorkspaceInfo(root, installWorkspaceName, workspacesMap); + if (!installWorkspaceInfo) { + throw new Error(`No workspaces found: --workspace=${installWorkspaceName}`); + } + root = installWorkspaceInfo.root; + roots.push(root); + } else { + roots = [ root, ...workspaceRoots ]; + } + for (const rootDir of roots) { + const nodeModules = path.join(rootDir, 'node_modules'); + console.log('[npmupdate] removing %s', nodeModules); + await rimraf(nodeModules); + } console.log('[npmupdate] reinstall on %s', root); // make sure install ignore all package names diff --git a/lib/format_install_options.js b/lib/format_install_options.js index 613daeee..534dcdec 100644 --- a/lib/format_install_options.js +++ b/lib/format_install_options.js @@ -151,7 +151,5 @@ module.exports = function formatInstallOptions(options) { options.pruneCount = 0; options.pruneSize = 0; - debug('options: %j', options); - return options; }; diff --git a/lib/link.js b/lib/link.js index 5ff966e8..fbff710a 100644 --- a/lib/link.js +++ b/lib/link.js @@ -1,9 +1,7 @@ const debug = require('util').debuglog('npminstall:link'); -const utils = require('./utils'); const path = require('path'); -const { - getAliasPackageName, -} = require('./alias'); +const utils = require('./utils'); +const { getAliasPackageName } = require('./alias'); module.exports = async (parentDir, pkg, realDir, alias) => { // parentDir: node_modules/.store/utility@1.17.0/node_modules/utility diff --git a/lib/local_install.js b/lib/local_install.js index 1eaf4863..cf4daecd 100644 --- a/lib/local_install.js +++ b/lib/local_install.js @@ -126,6 +126,7 @@ async function _install(options, context) { } debug(`about to locally install pkgs (production: ${options.production}, client: ${options.client}): ${JSON.stringify(pkgs, null, 2)}`); } else { + debug('pkgs: %o', pkgs); // try to fix no version package from rootPkgDependencies const allDeps = rootPkgDependencies.allMap; for (const childPkg of pkgs) { @@ -151,7 +152,7 @@ async function _install(options, context) { await pMap(pkgs, mapper, 10); options.downloadFinished = Date.now(); - options.spinner && options.spinner.succeed(`Installed ${pkgs.length} packages`); + options.spinner && options.spinner.succeed(`Installed ${pkgs.length} packages on ${options.root}`); if (!options.disableDedupe) { // dedupe mode https://docs.npmjs.com/cli/dedupe @@ -194,6 +195,8 @@ async function _install(options, context) { // print install finished finishInstall(options); + + await utils.removeInstallDone(options.root); } async function installOne(parentDir, childPkg, options, context) { @@ -224,6 +227,13 @@ async function installOne(parentDir, childPkg, options, context) { } async function needInstall(parentDir, childPkg, options) { + // ignore workspace package + if (options.workspacesMap?.has(childPkg.name)) { + debug('workspace package(%s) exists, skip install', childPkg.name); + const { package } = options.workspacesMap.get(childPkg.name); + childPkg.workspacePackage = package; + return false; + } // always install if not install from package.json if (!options.installRoot) return true; diff --git a/lib/utils.js b/lib/utils.js index 1929c9bc..4635446b 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -5,9 +5,12 @@ const path = require('path'); const cp = require('child_process'); const { promisify } = require('util'); const { parse: urlparse } = require('url'); +const url = require('url'); const querystring = require('querystring'); -const tar = require('tar'); const zlib = require('zlib'); +const chalk = require('chalk'); +const globby = require('globby'); +const tar = require('tar'); const runscript = require('runscript'); const homedir = require('node-homedir'); const fse = require('fs-extra'); @@ -15,7 +18,6 @@ const destroy = require('destroy'); const normalizeData = require('normalize-package-data'); const semver = require('semver'); const utility = require('utility'); -const url = require('url'); const globalConfig = require('./config'); const get = require('./get'); @@ -94,6 +96,17 @@ exports.unsetInstallDone = async pkgRoot => { }); }; +exports.removeInstallDone = async pkgRoot => { + const pkgFile = path.join(pkgRoot, 'package.json'); + if (!(await exports.exists(pkgFile))) return; + const pkg = await exports.readJSON(pkgFile); + if (!(INSTALL_DONE_KEY in pkg)) return; + + await exports.addMetaToJSONFile(pkgFile, { + [INSTALL_DONE_KEY]: undefined, + }); +}; + // 判断 pkg 是否已经安装完成 exports.isInstallDone = async pkgRoot => { const pkg = await exports.readJSON(path.join(pkgRoot, 'package.json')); @@ -106,7 +119,7 @@ exports.addMetaToJSONFile = async (filepath, meta) => { for (const key in meta) { pkg[key] = meta[key]; } - await fs.writeFile(filepath, JSON.stringify(pkg, null, 2)); + await fs.writeFile(filepath, JSON.stringify(pkg, null, 2) + '\n'); }; exports.mkdirp = async dir => { @@ -580,3 +593,72 @@ exports.getDisplayName = (pkg, ancestors) => { }; exports.exec = promisify(cp.exec); + +exports.readWorkspaces = async root => { + const workspaceInfos = []; + const rootPkgFile = path.join(root, 'package.json'); + const rootPkg = await exports.readJSON(rootPkgFile); + if (Array.isArray(rootPkg.workspaces) && rootPkg.workspaces.length > 0) { + // should contains package.json + const patterns = rootPkg.workspaces.map(workspace => { + // 'packages/*', 'packages/*/' + return workspace + (workspace.endsWith('/') ? '' : '/') + 'package.json'; + }); + const workspacePkgFiles = await globby(patterns, { + cwd: root, + gitignore: true, + }); + debug('[readWorkspaces] glob %o => %o', patterns, workspacePkgFiles); + for (const workspacePkgName of workspacePkgFiles) { + const workspacePkgFile = path.join(root, workspacePkgName); + const workspacePkg = await exports.readJSON(workspacePkgFile); + if (!workspacePkg.name) { + console.warn(chalk.yellow('npminstall WARN: workspace(%s) not found or missing `name` property'), workspacePkgFile); + continue; + } + const workspaceRoot = path.dirname(workspacePkgFile); + workspaceInfos.push({ + root: workspaceRoot, + package: workspacePkg, + }); + } + } + + // sort by dependencies/devDependencies/peerDependencies relations + const beforeSortNames = workspaceInfos.map(info => info.package.name); + workspaceInfos.sort((a, b) => { + if (a.package.dependencies?.[b.package.name] || + a.package.devDependencies?.[b.package.name] || + a.package.peerDependencies?.[b.package.name]) { + return 1; + } + return -1; + }); + const afterSortNames = workspaceInfos.map(info => info.package.name); + debug('workspaces sort %j => %j', beforeSortNames, afterSortNames); + + const workspaceRoots = []; + const workspacesMap = new Map(); + for (const info of workspaceInfos) { + workspaceRoots.push(info.root); + workspacesMap.set(info.package.name, info); + } + return { + workspaceRoots, + workspacesMap, + }; +}; + +exports.getWorkspaceInfo = async (root, workspaceNameOrPath, workspacesMap = null) => { + if (!workspacesMap) { + const rootInfo = await exports.readWorkspaces(root); + workspacesMap = rootInfo.workspacesMap; + } + let workspaceInfo = workspacesMap.get(workspaceNameOrPath); + if (!workspaceInfo) { + // try to use `/package.json` + const workspacePkg = await exports.readJSON(path.join(root, workspaceNameOrPath, 'package.json')); + workspaceInfo = workspacePkg.name && workspacesMap.get(workspacePkg.name); + } + return workspaceInfo; +}; diff --git a/package.json b/package.json index c9d19e77..3f233256 100644 --- a/package.json +++ b/package.json @@ -17,10 +17,12 @@ "scripts": { "contributor": "git-contributor", "test": "npm run lint && egg-bin test -t 2000000 -p", - "test-cov": "egg-bin cov -t 2000000 -p", - "test-local": "npm run clean && npm_china=true local=true egg-bin test -t 2000000 --full-trace -p", + "test-local": "npm run test-local-single -- -p", + "test-local-single": "npm run clean && npm_china=true local=true egg-bin test -t 2000000 --full-trace", + "cov": "egg-bin cov -t 2000000", + "cov-debug": "cross-env NODE_DEBUG=npminstall* egg-bin cov -t 2000000", "lint": "eslint . --fix", - "ci": "npm run lint && npm run test-cov", + "ci": "npm run lint && npm run cov", "clean": "rm -rf test/fixtures/.tmp*" }, "dependencies": { @@ -35,6 +37,7 @@ "destroy": "^1.0.4", "detect-libc": "^2.0.1", "fs-extra": "^7.0.1", + "globby": "^11.1.0", "minimatch": "^3.0.4", "minimist": "^1.2.0", "moment": "^2.24.0", @@ -56,6 +59,7 @@ }, "devDependencies": { "coffee": "5", + "cross-env": "^7.0.3", "egg-bin": "5", "eslint": "8", "eslint-config-egg": "12", diff --git a/test/fixtures/npm-workspaces/core/bar/package.json b/test/fixtures/npm-workspaces/core/bar/package.json new file mode 100644 index 00000000..feac5e04 --- /dev/null +++ b/test/fixtures/npm-workspaces/core/bar/package.json @@ -0,0 +1,10 @@ +{ + "name": "bar", + "version": "1.0.0", + "dependencies": { + "foo": "1.0.0" + }, + "scripts": { + "postinstall": "node -p \"require('foo')\"" + } +} diff --git a/test/fixtures/npm-workspaces/core/foo/index.js b/test/fixtures/npm-workspaces/core/foo/index.js new file mode 100644 index 00000000..043ee974 --- /dev/null +++ b/test/fixtures/npm-workspaces/core/foo/index.js @@ -0,0 +1,4 @@ +module.exports = { + name: 'foo', + pedding: require('pedding/package.json'), +}; diff --git a/test/fixtures/npm-workspaces/core/foo/package.json b/test/fixtures/npm-workspaces/core/foo/package.json new file mode 100644 index 00000000..08c05250 --- /dev/null +++ b/test/fixtures/npm-workspaces/core/foo/package.json @@ -0,0 +1,7 @@ +{ + "name": "foo", + "version": "1.0.0", + "dependencies": { + "pedding": "^1.0.0" + } +} diff --git a/test/fixtures/npm-workspaces/core/scoped/package.json b/test/fixtures/npm-workspaces/core/scoped/package.json new file mode 100644 index 00000000..472defe9 --- /dev/null +++ b/test/fixtures/npm-workspaces/core/scoped/package.json @@ -0,0 +1,10 @@ +{ + "name": "@cnpm/foo", + "version": "1.0.0", + "dependencies": { + "foo": "1.0.0" + }, + "scripts": { + "postinstall": "node -p \"require('foo')\"" + } +} diff --git a/test/fixtures/npm-workspaces/lib/index.js b/test/fixtures/npm-workspaces/lib/index.js new file mode 100644 index 00000000..203fed43 --- /dev/null +++ b/test/fixtures/npm-workspaces/lib/index.js @@ -0,0 +1,5 @@ +const moduleA = require('aa'); +console.log(moduleA, require.resolve('aa')); +const moduleB = require('b'); +console.log(moduleB, require.resolve('b')); +console.log(require.resolve('abbrev-range')); diff --git a/test/fixtures/npm-workspaces/package-init.json b/test/fixtures/npm-workspaces/package-init.json new file mode 100644 index 00000000..db012f09 --- /dev/null +++ b/test/fixtures/npm-workspaces/package-init.json @@ -0,0 +1,18 @@ +{ + "name": "my-workspaces-powered-project", + "version": "1.0.0", + "scripts": { + "postinstall": "npm run test", + "test": "node lib/index.js" + }, + "workspaces": [ + "packages/a", + "packages/b", + "packages/c", + "packages/d", + "core/*" + ], + "dependencies": { + "abbrev-range": "^1.0.0" + } +} diff --git a/test/fixtures/npm-workspaces/packages/a/index.js b/test/fixtures/npm-workspaces/packages/a/index.js new file mode 100644 index 00000000..01d042fc --- /dev/null +++ b/test/fixtures/npm-workspaces/packages/a/index.js @@ -0,0 +1 @@ +module.exports = 'packages/a'; diff --git a/test/fixtures/npm-workspaces/packages/a/package.json b/test/fixtures/npm-workspaces/packages/a/package.json new file mode 100644 index 00000000..4c0cdcf1 --- /dev/null +++ b/test/fixtures/npm-workspaces/packages/a/package.json @@ -0,0 +1,10 @@ +{ + "name": "aa", + "version": "1.0.0", + "scripts": { + "test": "node -p \"console.log(require('./index'))\"" + }, + "dependencies": { + "abbrev": "2.0.0" + } +} diff --git a/test/fixtures/npm-workspaces/packages/b/index.js b/test/fixtures/npm-workspaces/packages/b/index.js new file mode 100644 index 00000000..f45c0f55 --- /dev/null +++ b/test/fixtures/npm-workspaces/packages/b/index.js @@ -0,0 +1,7 @@ +module.exports = { + name: 'b', + abbrev: { + version: require('abbrev/package.json').version, + path: require.resolve('abbrev'), + }, +} diff --git a/test/fixtures/npm-workspaces/packages/b/package.json b/test/fixtures/npm-workspaces/packages/b/package.json new file mode 100644 index 00000000..916eb9e5 --- /dev/null +++ b/test/fixtures/npm-workspaces/packages/b/package.json @@ -0,0 +1,16 @@ +{ + "name": "b", + "version": "1.0.0", + "description": "", + "main": "index.js", + "dependencies": { + "abbrev": "1.1.1" + }, + "devDependencies": {}, + "scripts": { + "postinstall": "npm run test", + "test": "node -e \"console.log(require('./index'))\"" + }, + "author": "", + "license": "ISC" +} diff --git a/test/fixtures/npm-workspaces/packages/d/index.js b/test/fixtures/npm-workspaces/packages/d/index.js new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/npm-workspaces/packages/d/package.json b/test/fixtures/npm-workspaces/packages/d/package.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/test/fixtures/npm-workspaces/packages/d/package.json @@ -0,0 +1 @@ +{} diff --git a/test/helper.js b/test/helper.js index e0da9cd4..dc0dd977 100644 --- a/test/helper.js +++ b/test/helper.js @@ -29,5 +29,6 @@ exports.tmp = name => { }; exports.npminstall = path.join(__dirname, '..', 'bin', 'install.js'); +exports.npmupdate = path.join(__dirname, '..', 'bin', 'update.js'); exports.readJSON = require('../lib/utils').readJSON; diff --git a/test/install-workspaces.test.js b/test/install-workspaces.test.js new file mode 100644 index 00000000..4dd7a47c --- /dev/null +++ b/test/install-workspaces.test.js @@ -0,0 +1,241 @@ +const assert = require('assert'); +const path = require('path'); +const fs = require('fs/promises'); +const fse = require('fs-extra'); +const coffee = require('coffee'); +const { rimraf } = require('../lib/utils'); +const helper = require('./helper'); + +describe('test/install-workpsaces.test.js', () => { + const root = helper.fixtures('npm-workspaces'); + const [ tmp, cleanupTmpDir ] = helper.tmp(); + const cleanup = helper.cleanup(root); + + beforeEach(async () => { + await cleanup(); + await cleanupTmpDir(); + await rimraf(path.join(root, 'packages/a/node_modules')); + await rimraf(path.join(root, 'packages/b/node_modules')); + await rimraf(path.join(root, 'core/foo/node_modules')); + await rimraf(path.join(root, 'core/bar/node_modules')); + await rimraf(path.join(root, 'packages/c')); + await fs.copyFile(path.join(root, 'package-init.json'), path.join(root, 'package.json')); + await fse.copy(root, tmp); + }); + + it('should install all on root', async () => { + await coffee.fork(helper.npminstall, [], { cwd: root }) + .debug() + .expect('code', 0) + .end(); + + let pkg = await helper.readJSON(path.join(root, 'node_modules/aa/package.json')); + assert.equal(pkg.name, 'aa'); + pkg = await helper.readJSON(path.join(root, 'node_modules/aa/node_modules/abbrev/package.json')); + assert.equal(pkg.name, 'abbrev'); + assert.equal(pkg.version, '2.0.0'); + pkg = await helper.readJSON(path.join(root, 'packages/a/node_modules/abbrev/package.json')); + assert.equal(pkg.name, 'abbrev'); + assert.equal(pkg.version, '2.0.0'); + pkg = await helper.readJSON(path.join(root, 'node_modules/b/package.json')); + assert.equal(pkg.name, 'b'); + pkg = await helper.readJSON(path.join(root, 'node_modules/b/node_modules/abbrev/package.json')); + assert.equal(pkg.name, 'abbrev'); + assert.equal(pkg.version, '1.1.1'); + pkg = await helper.readJSON(path.join(root, 'packages/b/node_modules/abbrev/package.json')); + assert.equal(pkg.name, 'abbrev'); + assert.equal(pkg.version, '1.1.1'); + pkg = await helper.readJSON(path.join(root, 'node_modules/abbrev-range/package.json')); + assert.equal(pkg.name, 'abbrev-range'); + pkg = await helper.readJSON(path.join(root, 'node_modules/foo/package.json')); + assert.equal(pkg.name, 'foo'); + pkg = await helper.readJSON(path.join(root, 'node_modules/bar/package.json')); + assert.equal(pkg.name, 'bar'); + // foo don't install, it was workspace package + pkg = await helper.readJSON(path.join(root, 'node_modules/bar/node_modules/foo/package.json')); + assert.equal(pkg.name, undefined); + pkg = await helper.readJSON(path.join(root, 'node_modules/@cnpm/foo/package.json')); + assert.equal(pkg.name, '@cnpm/foo'); + pkg = await helper.readJSON(path.join(root, 'node_modules/@cnpm/foo/node_modules/foo/package.json')); + assert.equal(pkg.name, undefined); + }); + + it('should install new package on one workspace', async () => { + const pkgDir = path.join(root, 'packages/c'); + await fs.mkdir(pkgDir, { recursive: true }); + const pkgFile = path.join(pkgDir, 'package.json'); + await fs.writeFile(pkgFile, JSON.stringify({ + name: 'c', + })); + // npm install abbrev -w c + await coffee.fork(helper.npminstall, [ 'abbrev', '-w', 'c' ], { cwd: root }) + .debug() + .expect('code', 0) + .end(); + let pkg = await helper.readJSON(pkgFile); + assert.equal(pkg.name, 'c'); + assert.equal(typeof pkg.dependencies.abbrev, 'string'); + + await coffee.fork(helper.npminstall, [ 'abbrev@1.1.1', '-w', 'c' ], { cwd: root }) + .debug() + .expect('code', 0) + .end(); + pkg = await helper.readJSON(pkgFile); + assert.equal(pkg.name, 'c'); + assert.equal(pkg.dependencies.abbrev, '^1.1.1'); + + // should support workspace-path + await coffee.fork(helper.npminstall, [ 'abbrev@1.1.0', '--workspace', 'packages/c' ], { cwd: root }) + .debug() + .expect('code', 0) + .end(); + pkg = await helper.readJSON(pkgFile); + assert.equal(pkg.name, 'c'); + assert.equal(pkg.dependencies.abbrev, '^1.1.0'); + }); + + it('should install new package on all workspaces', async () => { + // npm install pedding --workspaces + await coffee.fork(helper.npminstall, [ 'pedding', '--workspaces' ], { cwd: tmp }) + .debug() + .expect('code', 0) + .end(); + let pkg = await helper.readJSON(path.join(tmp, 'packages/a/package.json')); + assert.equal(pkg.name, 'aa'); + assert.equal(typeof pkg.dependencies.pedding, 'string'); + pkg = await helper.readJSON(path.join(tmp, 'packages/b/package.json')); + assert.equal(pkg.name, 'b'); + assert.equal(typeof pkg.dependencies.pedding, 'string'); + pkg = await helper.readJSON(path.join(tmp, 'core/foo/package.json')); + assert.equal(pkg.name, 'foo'); + assert.equal(typeof pkg.dependencies.pedding, 'string'); + + await coffee.fork(helper.npminstall, [ 'pedding@1.0.0', '--workspaces', '-D' ], { cwd: tmp }) + .debug() + .expect('code', 0) + .end(); + pkg = await helper.readJSON(path.join(tmp, 'packages/a/package.json')); + assert.equal(pkg.name, 'aa'); + assert.equal(pkg.devDependencies.pedding, '^1.0.0'); + pkg = await helper.readJSON(path.join(tmp, 'packages/b/package.json')); + assert.equal(pkg.name, 'b'); + assert.equal(pkg.devDependencies.pedding, '^1.0.0'); + pkg = await helper.readJSON(path.join(tmp, 'core/foo/package.json')); + assert.equal(pkg.name, 'foo'); + assert.equal(pkg.devDependencies.pedding, '^1.0.0'); + }); + + it('should install workspace-package on one workspace', async () => { + const pkgDir = path.join(root, 'packages/c'); + await fs.mkdir(pkgDir, { recursive: true }); + const pkgFile = path.join(pkgDir, 'package.json'); + await fs.writeFile(pkgFile, JSON.stringify({ + name: 'c', + })); + await coffee.fork(helper.npminstall, [ 'aa', '-w', 'c' ], { cwd: root }) + .debug() + .expect('code', 0) + .end(); + let pkg = await helper.readJSON(pkgFile); + assert.equal(pkg.name, 'c'); + assert.equal(pkg.dependencies.aa, '^1.0.0'); + // add dependencies only + pkg = await helper.readJSON(path.join(root, 'node_modules/aa/package.json')); + assert.equal(pkg.name, 'aa'); + pkg = await helper.readJSON(path.join(root, 'packages/c/node_modules/aa/package.json')); + assert.equal(pkg.name, undefined); + await coffee.fork(helper.npminstall, [ 'aa@1', '-w', 'c' ], { cwd: root }) + .debug() + .expect('code', 0) + .end(); + pkg = await helper.readJSON(pkgFile); + assert.equal(pkg.name, 'c'); + assert.equal(pkg.dependencies.aa, '^1.0.0'); + // wrong version should work + await coffee.fork(helper.npminstall, [ 'aa@2', '-w', 'c' ], { cwd: root }) + .debug() + .expect('code', 0) + .end(); + pkg = await helper.readJSON(pkgFile); + assert.equal(pkg.name, 'c'); + assert.equal(pkg.dependencies.aa, '^1.0.0'); + // scoped package work + await coffee.fork(helper.npminstall, [ '@cnpm/foo', '-w', 'c', '--save-dev' ], { cwd: root }) + .debug() + .expect('code', 0) + .end(); + pkg = await helper.readJSON(pkgFile); + assert.equal(pkg.name, 'c'); + assert.equal(pkg.devDependencies['@cnpm/foo'], '^1.0.0'); + }); + + it('should install workspace-package on root', async () => { + const pkgFile = path.join(root, 'package.json'); + await coffee.fork(helper.npminstall, [ 'aa' ], { cwd: root }) + .debug() + .expect('code', 0) + .end(); + let pkg = await helper.readJSON(pkgFile); + assert.equal(pkg.dependencies.aa, '^1.0.0'); + // scoped package work + await coffee.fork(helper.npminstall, [ '@cnpm/foo' ], { cwd: root }) + .debug() + .expect('code', 0) + .end(); + pkg = await helper.readJSON(pkgFile); + assert.equal(pkg.dependencies['@cnpm/foo'], '^1.0.0'); + await coffee.fork(helper.npminstall, [], { cwd: root }) + .debug() + .expect('code', 0) + .end(); + }); + + it('should throw error when workspace not exists', async () => { + await coffee.fork(helper.npminstall, [ 'abbrev', '--workspace', 'not-exists' ], { cwd: root }) + .debug() + .expect('code', 1) + .expect('stderr', /No workspaces found: --workspace=not-exists/) + .end(); + }); + + it('should update all on root', async () => { + await coffee.fork(helper.npmupdate, [], { cwd: root }) + .debug() + .expect('code', 0) + .expect('stdout', /\[npmupdate] removing/) + .end(); + + let pkg = await helper.readJSON(path.join(root, 'node_modules/aa/package.json')); + assert.equal(pkg.name, 'aa'); + pkg = await helper.readJSON(path.join(root, 'node_modules/aa/node_modules/abbrev/package.json')); + assert.equal(pkg.name, 'abbrev'); + assert.equal(pkg.version, '2.0.0'); + }); + + it('should update one workspace', async () => { + await coffee.fork(helper.npmupdate, [ '-w', 'aa' ], { cwd: root }) + .debug() + .expect('code', 0) + .expect('stdout', /\[npmupdate] removing/) + .end(); + + let pkg = await helper.readJSON(path.join(root, 'node_modules/aa/package.json')); + assert.equal(pkg.name, 'aa'); + pkg = await helper.readJSON(path.join(root, 'node_modules/aa/node_modules/abbrev/package.json')); + assert.equal(pkg.name, 'abbrev'); + assert.equal(pkg.version, '2.0.0'); + // dont install b deps + pkg = await helper.readJSON(path.join(root, 'node_modules/b/node_modules/abbrev/package.json')); + assert.equal(pkg.name, undefined); + + // support workpsace-path + await coffee.fork(helper.npmupdate, [ '-w', 'packages/a' ], { cwd: root }) + .debug() + .expect('code', 0) + .expect('stdout', /\[npmupdate] removing/) + .end(); + pkg = await helper.readJSON(path.join(root, 'node_modules/aa/node_modules/abbrev/package.json')); + assert.equal(pkg.name, 'abbrev'); + assert.equal(pkg.version, '2.0.0'); + }); +});