Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
ruyadorno committed Mar 2, 2021
1 parent 52c4d3f commit f007920
Show file tree
Hide file tree
Showing 11 changed files with 376 additions and 3 deletions.
6 changes: 5 additions & 1 deletion lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ module.exports = (process) => {

const cmd = npm.argv.shift()
const impl = npm.commands[cmd]
if (impl)
const workspaces = npm.config.get('workspace').length

if (workspaces)
npm.commands.workspaces([cmd, ...npm.argv], errorHandler)
else if (impl)
impl(npm.argv, errorHandler)
else {
npm.config.set('usage', false)
Expand Down
3 changes: 2 additions & 1 deletion lib/npm.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const Config = require('@npmcli/config')
require('graceful-fs').gracefulify(require('fs'))

const procLogListener = require('./utils/proc-log-listener.js')
const readFlatOptions = require('./utils/flat-options.js')

const proxyCmds = new Proxy({}, {
get: (target, cmd) => {
Expand Down Expand Up @@ -199,7 +200,7 @@ const npm = module.exports = new class extends EventEmitter {
}

get flatOptions () {
return require('./utils/flat-options.js')(this)
return readFlatOptions(this)
}

get lockfileVersion () {
Expand Down
2 changes: 1 addition & 1 deletion lib/repo.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class Repo {

async repo (args) {
if (!args || !args.length)
args = ['.']
args = [`file:${this.npm.flatOptions.prefix}`]

await Promise.all(args.map(pkg => this.get(pkg)))
}
Expand Down
3 changes: 3 additions & 0 deletions lib/utils/cmd-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const shorthands = {
'clean-install-test': 'cit',
x: 'exec',
why: 'explain',
ws: 'workspaces',
}

const affordances = {
Expand Down Expand Up @@ -133,6 +134,8 @@ const cmdList = [
'doctor',
'exec',
'explain',

'workspaces',
]

const plumbing = ['birthday', 'help-search']
Expand Down
2 changes: 2 additions & 0 deletions lib/utils/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ const defaults = {
version: false,
versions: false,
viewer: isWindows ? 'browser' : 'man',
workspace: [],
}

const types = {
Expand Down Expand Up @@ -348,6 +349,7 @@ const types = {
version: Boolean,
versions: Boolean,
viewer: String,
workspace: [String, Array],
}

const shorthands = {
Expand Down
2 changes: 2 additions & 0 deletions lib/utils/flat-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ const flatten = obj => ({
// respected if this is not set.
proxy: obj['https-proxy'] || obj.proxy,
noProxy: obj.noproxy,

workspace: obj.workspace,
})

const flatOptions = npm => Object.freeze({
Expand Down
87 changes: 87 additions & 0 deletions lib/workspaces.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
const { resolve } = require('path')
const mapWorkspaces = require('@npmcli/map-workspaces')
const rpj = require('read-package-json-fast')

const npm = require('./npm.js')
const usageUtil = require('./utils/usage.js')

const wsCmds = new Map(Object.entries({
docs: require('./workspaces/default.js'),
doctor: require('./workspaces/default.js'),
diff: require('./workspaces/default.js'),
'dist-tag': require('./workspaces/default.js'),
pack: require('./workspaces/default.js'),
publish: require('./workspaces/default.js'),
repo: require('./workspaces/default.js'),
'set-script': require('./workspaces/default.js'),
unpublish: require('./workspaces/default.js'),
version: require('./workspaces/default.js'),
view: require('./workspaces/default.js'),
ls: require('./workspaces/ls.js'),
}))

class Workspaces {
constructor (npm) {
this.npm = npm
}

get usage () {
return usageUtil('npm ws <cmd>')
}

exec (args, cb) {
this.workspaces(args).then(() => cb()).catch(cb)
}

async workspaces (args) {
if (npm.flatOptions.global) {
throw Object.assign(
new Error('`npm workspaces` does not support global packages'),
{ code: 'ENOGLOBAL' }
)
}

if (!args.length) {
throw Object.assign(
new Error('`npm workspaces` needs at least a command to run'),
{ code: 'ENOWSCMD' }
)
}

const cwd = npm.flatOptions.prefix
const pkg = await rpj(resolve(npm.flatOptions.prefix, 'package.json'))
const workspaces = await mapWorkspaces({ cwd, pkg })

if (npm.flatOptions.workspace) {
for (const workspaceArg of npm.flatOptions.workspace) {
for (const [key, path] of workspaces.entries()) {
if (workspaceArg !== key
&& resolve(npm.flatOptions.prefix, workspaceArg) !== path)
workspaces.delete(key)
}
}
}

let [cmdName, ...cmdArgs] = args

if (cmdName === 'workspaces')
cmdName = cmdArgs.shift(1)

if (wsCmds.has(cmdName)) {
await wsCmds.get(cmdName)({
args: cmdArgs,
cmdName,
npm: this.npm,
workspaces,
})
return
}

throw Object.assign(
new Error(`${cmdName} is not a recognized command`),
{ code: 'EBADWSCMD' }
)
}
}

module.exports = Workspaces
23 changes: 23 additions & 0 deletions lib/workspaces/default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const { resolve } = require('path')

const cmd = ({ npm, path, cmdName, args }) =>
new Promise((res, reject) => {
const wsPath = resolve(npm.flatOptions.prefix, path)
npm.localPrefix = wsPath
npm.config.set('prefix', wsPath)

npm.commands[cmdName](args, err => {
if (err)
reject(err)

res()
})
})

const runCmd = ({ npm, workspaces, cmdName, args }) =>
Promise.all(
[...workspaces.values()]
.map(w => cmd({ npm, path: w, cmdName, args }))
)

module.exports = runCmd
17 changes: 17 additions & 0 deletions lib/workspaces/ls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const npm = require('../npm.js')

const cmd = (args) =>
new Promise((res, reject) => {
npm.commands.ls(args, err => {
if (err)
reject(err)

res()
})
})

const runCmd = async (workspaces, args) => {
await cmd([...workspaces.keys(), ...args])
}

module.exports = runCmd
125 changes: 125 additions & 0 deletions notes/RFC_FOLLOWUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
### Syntax

- `npm ws fund` (runs a top-level cmd in all configured workspaces)
- `npm ws fund --workspace=a` (runs a top-level cmd in the context of a workspace)
- `npm fund -w a` (top-level `ws` becomes redundant if using `-w` and may be omitted)

Adding a top-level `workspaces|ws` command should abstract enough the implementation to make it flexible enough to accomodate future tweaks in the workspace installing algorithm.

[Update]: Maybe add a separate top-level command for configuration management? e.g: `npm wsc add|rm|ls`

#### Where to start?
- [ ] Add new `workspaces|ws` command/alias
- [ ] Add new folder `./lib/workspaces/*.js`
- [ ] Add default behavior that sets `prefix` to top-level commands under `./lib/workspaces/default.js`
- [ ] Add `ws run-script`
- [ ] Add `ws install` (ie. **scoped installs**)
- [ ] Add `ws install <pkg>`
- [ ] Add `ws ci`
- [ ] Add `ws update`
- [ ] Add `ws uninstall`
- [ ] Add `ws outdated` (ie. `cd ./ && npm outdated`, might need tweaking Arborist to only load from a specific tree node)
- [ ] Add `ws ls` (ie. `cd ./ && npm ls`)
- [ ] Add ``

#### What should happen?
- [ ] Should run commands over multiple workspaces
* (no args) run command across **all** workspaces
* (`-w=<workspace-name>` named option) filter by only defined names
- [ ] Should support `--parallel` (defaults to `--serial`)
- ref. https://www.npmjs.com/package/npm-run-all

#### What happens under the hood?

```
./lib/workspaces.js `npm workspaces|ws`
./lib/workspaces/default.js <- default... tries `prefix` + <command> / warn if we couldn't do anything...
./lib/workspaces/install.js <- some cmds require special logic
./lib/workspaces/publish.js <- would set `prefix` & then include ./lib/publish.js
...
# The default behavior is to run the command setting the prefix to workspace realpath, e.g:
npm ws publish -w name
# Might be effectively the same as:
npm publish --prefix=<workspace-name>
# Assuming `npm publish` is a command that won't need special tweaks/impl
npm ws install -w name
# ^--- "scoped install": *only* reify the packages for the workspace defined, e.g:
root:
dependencies:
d@1.0.0
workspaces:
a -> foo@^1.0.0 -> c@1
b -> foo@^1.0.1 -> c@2
$ npm ws install -w a
node_modules
+- a -> ../a
+- c@2
+- foo@1.0.1
# NOTE: just be mindful of deduping (ie. you'd get c@2 if all workspaces
# were being installed... you should still get it if you only specify `a`)
# NOTE2: arborist will not place `d` within `node_modules` for
# a "scoped install"
# Adding a new dep to a workspace:
$ npm ws install -w <workspace-name> <pkg> -> ./lib/workspaces/install.js
# ^--- <pkg> will be installed as a
# dep of workspace-name
```

#### Adding a new dep to a workspace:

```
npm install <pkg>
Arborist
root:
- <pkg> <-- add user request
npm ws install <pkg> -w <workspace-name>
Arborist
root:
- workspace-name:
- <pkg> <-- add user request under workspace-name instead
```

#### API:

```
npm ws <command> -w|--workspace=<pkg-name|group-alias>
```

#### Groups:

A simple way to refer to a set of workspace by using a single name, e.g:

```
.
+- core
+- foo
+- plugins
+- lorem
+- ipsum
```

With a root `package.json` defining both workspaces packages and groups:
```json
{
"name": "workspace-example",
"version": "1.0.0",
"workspaces": {
"groups": {
"plugins": ["lorem", "ipsum"],
"common": ["foo"]
},
"packages": [
"core/*",
"plugins/*"
]
}
}
```

Running: `npm ws install abbrev -w plugins` effectively means adding abbrev as a dep to both `lorem` and `ipsum` and reifying the tree.

0 comments on commit f007920

Please sign in to comment.