Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI + Scoped Packages #1

Merged
merged 6 commits into from
Nov 21, 2017
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 31 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,34 @@
# symlinked
Node utility to list symlinks made by [`npm link`](https://docs.npmjs.com/cli/link), [`yarn link`](https://yarnpkg.com/lang/en/docs/cli/link/), or [`fs.link`](https://nodejs.org/api/fs.html)

## Setup
## CLI

### Install via `npm` or `yarn`
```
npm install --global symlinked
```

```
yarn add global symlinked
```
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yarn global

Unlike the --global flag in npm, global is a command which must immediately follow yarn. Entering yarn add global package-name will add the packages named global and package-name locally instead of adding package-name globally.

Copy link
Owner

@ryanve ryanve Nov 18, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of repeating the install instructions in both sections we can do something like this near the top. I think we can downplay the yarn instructions and focus on the npm ones. Local install applies to the CLI version if using via npm scripts or npx so I think we should favor local in the documentation. I can finetune the documentation afterwards if you want but would probably do something like in the comments below if you want to adjust.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Install

npm install symlinked

Local install as above is best practice if you are using in a shared codebase because then all developers will use the same version. CLI can be used locally via npx or via npm scripts. npm install has a --global flag you can add if you prefer global use. Yarn can be used via yarn add and yarn global respectively if you prefer yarn.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

npx example

npx symlinked --paths

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All good points, and good eye on the yarn global bit. Will drop these in and totally cool if you want to fine-tune.


### Usage

```
Usage: symlinked [<path>]

Finds all linked package names of an npm package.

Options:

-h, --help Display this usage info
-p, --paths Get linked package paths
-r, --roots Get linked package roots
-l, --links Get linked package links
```

## API

### Install via `npm` or `yarn`
```
npm install symlinked
Expand All @@ -16,16 +43,16 @@ yarn add symlinked
var symlinked = require("symlinked")
```

## Methods
### Methods
- `symlinked.names(dir: ".")` get array of linked package names
- `symlinked.paths(dir: ".")` get array of linked package paths
- `symlinked.roots(dir: ".")` get array of linked package roots
- `symlinked.links(dir: ".")` get array of linked package links
- `symlinked.is(path)` test if path exists and is linked
- `symlinked.read(path)` read link

## Examples
### Ran in package directory with `said` dependency linked on both ends
### Examples
#### Ran in package directory with `said` dependency linked on both ends
```js
symlinked.names()
// [ 'said' ]
Expand Down
60 changes: 60 additions & 0 deletions bin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env node

var symlinked = require("./")

var help = false
var dashdash = false
var error = null
var flags = []

var args = process.argv.slice(2).filter(function(arg) {
if (dashdash)
return !!arg
else if (arg === "--")
dashdash = true
else if (arg === "--paths" || arg === "-p")
flags.push("paths")
else if (arg === "--roots" || arg === "-r")
flags.push("roots")
else if (arg === "--links" || arg === "-l")
flags.push("links")
else if (arg.match(/^(-+|\/)(h(elp)?|\?)$/))
help = true
else if (arg.match(/^-/))
error = "Unknown flag: " + arg
else
return !!arg
})

if (help || args.length > 1 || flags.length > 1 || error) {
// If they didn't ask for help, then this is not a "success"
var log = help ? console.log : console.error
log("Usage: symlinked [<path>]")
log("")
log(" Finds all linked package names of an npm package.")
log("")
if (error) {
log(" " + error)
log("")
}
if (flags.length > 1) {
log(" Maximum of one flag may be passed at a time. Received: " + JSON.stringify(flags))
log("")
}
log("Options:")
log("")
log(" -h, --help Display this usage info")
log(" -p, --paths Get linked package paths")
log(" -r, --roots Get linked package roots")
log(" -l, --links Get linked package links")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking it might make sense to explicitly require using --names or at least support the flag. Now --names appears unknown because -n, --names Get linked package names is not provided.

$ node bin --names
Usage: symlinked [<path>]

  Finds all linked package names of an npm package.

  Unknown flag: --names

Options:

  -h, --help     Display this usage info
  -p, --paths    Get linked package paths
  -r, --roots    Get linked package roots
  -l, --links    Get linked package links

For easier documentation I think it makes sense for the CLI to mirror the JavaScript API as much as possible. Right now the symlinked export is an object but if a future release made it be a function then we might want to reserve the argless case for that. Requiring at least one flag seems safer at least as a first go. What do you think?

Copy link
Contributor Author

@cchamberlain cchamberlain Nov 18, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM. Thinking about it now, it seems like the interface should be switched to this. LMK your thoughts.

Usage: symlinked <command> [<path>]

  Finds all linked package names of an npm package.

Commands:

  names    Get linked package names
  paths    Get linked package paths
  roots    Get linked package roots
  links    Get linked package links

Options:

  -h, --help     Display this usage info

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this interface seems slightly more expressive and flexible for future needs 💯

What that your reasoning too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ryanve yeah, flags didn't align well to the underlying API. Flags could be either global (--help) or vary by command - the model I had would not have worked well with the latter. The bin.js code is simpler now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ryanve the message under that Usage text doesn't really fit anymore. Might want to just edit inline before merge.

process.exit(help ? 0 : 1)
} else
go(args[0] || ".")

function go (dir) {
var exec = symlinked[flags[0] || "names"]
var results = exec(dir)
for (var i = 0; i < results.length; i++) {
console.log(results[i])
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linting runs via pretest you can do npm install and then npm test to check linting

58:7  error  Expected indentation of 4 spaces but found 6  indent

}
11 changes: 10 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,23 @@ function search(dir) {
var context = path.resolve(dir)
if (!fs.existsSync(context)) return []
var contents = fs.readdirSync(context)
return contents.map(function(name) {
var scopedRoots = []
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seeing as the scope is part of the package name then I think it makes sense for the names result to include the scope. Right? Would additionally adding scope info like found.scope = "@example" be useful as an indicator?

Current result

$ npm link eol
$ npm link @songkick/promise-retry
$ node bin
eol
promise-retry

Expected result

$ npm link eol
$ npm link @songkick/promise-retry
$ node bin
eol
@songkick/promise-retry

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ryanve good catch, the scope should definitely be in the output, I'll get that fixed.

If you feel strong about the indicator I can add it but think it would cut some usability if you wanted to pipe this command to another command (rimraf for example). Think it would make for nice content to a --pretty or --verbose flag at some point in future. Conversely, a flag could be passed down the line to specify only raw output. Whichever road you prefer is cool.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ryanve just realized what you meant by found.scope =, going to add that. Can ignore the comment above.

var results = contents.filter(function (name) {
var isBin = name === ".bin"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reason for the bin check? I'm not sure I understand this part.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since .bin is a special folder (never an npm package itself), this is filtering it from the results to be processed in the following map step. Not filtering it adds extra work and could lead to other issues since the map step is expecting these folders to be packages (AFAIK).

var isScoped = name.charAt(0) === "@"
if (isScoped) scopedRoots.push(path.join(dir, name))
return !isScoped && !isBin
}).map(function(name) {
var relative = path.join(dir, name)
var found = new Found
found.name = name
found.path = path.resolve(relative)
if (is(relative)) found.link = read(relative)
return found
})
return scopedRoots.reduce(function (_, scopedRoot) {
return _.concat(search(scopedRoot))
}, results)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought this was lodash at first so if reducing let's avoid _ as the accumulation variable name. I actually wonder if there is another technique we can use above that would avoid the need to reduce. Is the scoped solution to just search deep or is there more to it than that?

Copy link
Contributor Author

@cchamberlain cchamberlain Nov 19, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the filter step above was added to remove processing on scoped directories. For these, it needs to instead look up all their sub-directories (packages) and run it for them. This produces a multi-dimensional array.

Main point of the reduce is to flatten the arrays. Running search on each of the scopedRoots directories produces a new array of results.

@foo/
    a/
    b/
@bar
    c/
d/
e/

scopedRoots

[ "@foo", "@bar" ]
const scopedResults = scopedRoots.map(search);

scopedResults

[
    [ "@foo/a", "@foo/b" ],
    [ "@foo/c" ]
]

This would then need an extra step to reduce it to:

[
    "@foo/a",
    "@foo/b",
    "@foo/c"
]

So instead of all that, I used reduce to flatten as it searches each scoped directory. I'm sure there are other ways to rewrite it, but that's the gist of it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ryanve thinking about it more, I think the entire filter / map / reduce would be better as a single reduce. I'll kick something up in next commit.

}

function read(p) {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "0.3.0",
"description": "Node utility to list packages that are npm linked",
"main": "index.js",
"bin": "bin.js",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this flat structure. rimraf was a great example to follow 👍

"scripts": {
"demo": "node demo.js",
"lint": "eslint . --ext .js",
Expand Down