Skip to content

Commit a2c0fc5

Browse files
agorovyiisaacs
authored andcommitted
a request for npm run to traverse directory tree
PR-URL: #73 Credit: @agorovyi Close: #73 Reviewed-by: @isaacs EDIT(@isaacs): Added implementation details and clarified some of the context.
1 parent 2869e4b commit a2c0fc5

File tree

1 file changed

+139
-0
lines changed

1 file changed

+139
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# Let `npm run` traverse monorepo directory tree up to the root before failing
2+
3+
## Summary
4+
5+
Allow `npm run` command to traverse the directory tree up to the root level
6+
if it can't find a required `/node_modules/.bin` with executable command in
7+
subdirectory where it was executed from.
8+
9+
## Motivation
10+
11+
In the context of a multi-package monorepo where all dependencies are
12+
installed in the root `node_modules` it would've been very useful and
13+
convenient to have an ability to execute scripts from subdirectories that
14+
are packages within this monorepo without writing a full path to the root's
15+
`/node_modules/.bin` executable or adding a `prefix` to each run. Currently
16+
if you attempt to run a script from a subdirectory it will fail with
17+
`command not found` if there is no `/node_modules/.bin/` executable in the
18+
same subdirectory.
19+
20+
## Detailed Explanation
21+
22+
The expected change to `npm run` is to let it traverse the directory tree
23+
up to the root in search of an executable that may be stored in the root
24+
`/node_modules/.bin/`. If such executable doesn't exist anywhere in the
25+
directory tree then fail.
26+
27+
In `npm-lifecycle`, used by npm v6 and before, the `node_modules/.bin`
28+
folders are added to the `PATH` environment variable _only_ when contained
29+
within a nested `node_modules` structure. However, a package at
30+
`./packages/foo` would _not_ have `./node_modules/.bin` added to its script
31+
`PATH` environ.
32+
33+
### Example:
34+
35+
#### Folder structure:
36+
37+
```
38+
monorepo-root
39+
├─ node_modules
40+
│ └─ .bin
41+
│ ├─ react-docgen
42+
│ ├─ rimraf
43+
│ ├─ run-s
44+
│ └─ webpack
45+
├─ packages
46+
│ ├─ foo
47+
│ │ ├─ src
48+
│ │ └─ package.json
49+
│ ├─ bar
50+
│ │ ├─ src
51+
│ │ └─ package.json
52+
│ └─ baz
53+
│ ├─ src
54+
│ └─ package.json
55+
├─ package.json
56+
└─ package-lock.json
57+
```
58+
59+
#### scripts example for package `bar`
60+
61+
```json
62+
bar - package.json
63+
{
64+
"name": "@monorepo/bar",
65+
"scripts": {
66+
"build": "run-s clean prod comments",
67+
"prod": "BABEL_ENV=production webpack --mode production",
68+
"webpack:dev": "BABEL_ENV=development webpack --mode development",
69+
"build:dev": "run-s clean webpack:dev comments",
70+
"clean": "rimraf dist",
71+
"comments": "react-docgen ./src/components/ --exclude index.js --include Examples"
72+
}
73+
}
74+
```
75+
76+
With the above structure `npm run build` from bar directory will fail in
77+
npm v6 with `sh: run-s: command not found` as it can't find
78+
`node_modules/.bin/run-s` in that directory.
79+
80+
In npm v7, the PATH will include the root's `node_modules/.bin` folder, and
81+
so will run the command successfully.
82+
83+
## Rationale and Alternatives
84+
85+
The rationale for this feature is coming purely from a Developer Experience
86+
where you may have different teams working on different packages within one
87+
monorepo. In this case it is convenient for one team to work within one
88+
package folder and being able to run various scripts from that location
89+
without workarounds.
90+
91+
There are 3 alternative solutions that somewhat resolve this problem:
92+
93+
### Solution #1
94+
95+
Write full paths in `scripts` to point to appropriate executables. The
96+
disadvantage of this approach is that each `package.json` file may be
97+
polluted and it will be hard to maintain it going forward:
98+
99+
```json
100+
{
101+
"name": "@monorepo/bar",
102+
"scripts": {
103+
"build": "../../node_modules/.bin/run-s clean prod comments",
104+
"prod": "BABEL_ENV=production ../../node_modules/.bin/webpack --mode production",
105+
"webpack:dev": "BABEL_ENV=development ../../node_modules/.bin/webpack --mode development",
106+
"build:dev": "../../node_modules/.bin/run-s clean webpack:dev comments",
107+
"clean": "../../node_modules/.bin/rimraf dist",
108+
"comments": "../../node_modules/.bin/react-docgen ./src/components/ --exclude index.js --include Examples"
109+
}
110+
}
111+
```
112+
113+
### Solution #2
114+
115+
Use `--prefix ../..` each time when you run a script, e.g. `npm run
116+
--prefix ../.. build`.
117+
118+
This approach is a little bit cleaner than Solution #1 but still requires
119+
everyone to remember to prefix an executable. Working in large teams this
120+
`prefix` approach may be solved by documenting it but there is still a risk
121+
that people may forget to add `prefix`.
122+
123+
### Solution #3
124+
125+
Since this is a monorepo that may be managed with `lerna`, there is a way
126+
to execute these scripts from root by running `lerna run build` that will
127+
execute all build scripts in all packages or `lerna run build --scope bar`
128+
to execute it only in the `bar` package.
129+
130+
The disadvantage of this approach is that you need to constantly `cd`
131+
between root and your working directory and also scoping your script to a
132+
package that you work on.
133+
134+
## Implementation
135+
136+
The `@npmcli/run-script` module builds up the `PATH` environment variable
137+
including the `node_modules/.bin` included in _every_ parent directory,
138+
rather than merely adding the `.bin` folder included in every
139+
`node_modules` folder in the script working directory.

0 commit comments

Comments
 (0)