Skip to content

Commit

Permalink
feat: support npm workspaces (#433)
Browse files Browse the repository at this point in the history
https://docs.npmjs.com/cli/v9/using-npm/workspaces?v=true

- [x] install
- [x] install pkgname -w `<workspace-pacakge-name>`
- [x] install pkgname -w `<workspace-path>`
- [x] install `package-a` on `package-b` 
- [x] install `pacakge-a` on root
- [x] install `package` on all workspaces

```
# Install `lodash` on `package-a`
npm install lodash --workspace package-a

# Install `tap` on `package-b` as a dev dependency
npm install tap --workspace package-b --save-dev

# Install `package-a` on `package-b`
npm install package-a --workspace package-b

# Install `eslint` in all packages
npm install eslint --workspaces
```

- [x] work with demo
https://github.com/ruanmartinelli/npm-workspaces-demo
- [x] update
- [x] update -w a
- [x] postinstall
  • Loading branch information
fengmk2 authored Jan 5, 2023
1 parent dc60c7c commit a5d248e
Show file tree
Hide file tree
Showing 24 changed files with 552 additions and 35 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}


4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
99 changes: 85 additions & 14 deletions bin/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand All @@ -83,6 +87,7 @@ Object.assign(argv, parseArgs(originalArgv, {
c: 'china',
r: 'registry',
d: 'detail',
w: 'workspace',
},
})
);
Expand All @@ -98,6 +103,9 @@ Usage:
npminstall
npminstall <pkg>
npminstall <pkg> --workspace=<workspace>
npminstall <pkg> -w <workspace>
npminstall <pkg> --workspaces
npminstall <pkg>@<tag>
npminstall <pkg>@<version>
npminstall <pkg>@<version range>
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 = {
Expand All @@ -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);
}
}
}
}

}
}

Expand Down Expand Up @@ -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]
Expand Down
27 changes: 22 additions & 5 deletions bin/update.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(`
Expand All @@ -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
Expand Down
2 changes: 0 additions & 2 deletions lib/format_install_options.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,5 @@ module.exports = function formatInstallOptions(options) {
options.pruneCount = 0;
options.pruneSize = 0;

debug('options: %j', options);

return options;
};
6 changes: 2 additions & 4 deletions lib/link.js
Original file line number Diff line number Diff line change
@@ -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/[email protected]/node_modules/utility
Expand Down
12 changes: 11 additions & 1 deletion lib/local_install.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;

Expand Down
Loading

0 comments on commit a5d248e

Please sign in to comment.