Skip to content
This repository was archived by the owner on Aug 18, 2024. It is now read-only.

Commit 232555c

Browse files
authored
Merge pull request #16 from dominykas/tests
Add tests
2 parents 6f847d7 + 964ff7e commit 232555c

24 files changed

+563
-31
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
.idea
22
node_modules
3-
3+
test/tmp/

README.md

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,22 @@
22

33
Execute allowed `npm install` lifecycle scripts.
44

5-
## Usage
5+
## tl;dr
6+
7+
- Whitelist packages that you trust in your `package.json`: `"allowScripts": { "packageName": "1.x.x - 2.x.x" }`
8+
- Run `npm install --ignore-scripts` or `yarn install --ignore-scripts`
9+
- Run `npx allow-scripts`
10+
11+
Only the explicitly allowed `[pre|post]install` scripts will be executed.
612

7-
Run your `npm install` with `--ignore-scripts` (or add `ignore-scripts=true` in your `.npmrc`), then:
13+
14+
## Usage
815

916
```
1017
$ npx allow-scripts [--dry-run]
1118
```
1219

13-
Running the command will scan the list of installed dependencies (from the first source available: `npm-shrinkwrap.json`, `package-lock.json`, `npm ls --json`). It will then execute the scripts for allowed dependencies that have them in the following order:
20+
Running the command will scan the list of installed dependencies (using an existing `package-lock.json` or `npm-shrinkwrap.json` or by creating one on the fly). It will then execute the scripts for allowed dependencies that have them in the following order:
1421

1522
- `preinstall` in the main package
1623
- `preinstall` in dependencies
@@ -21,20 +28,21 @@ Running the command will scan the list of installed dependencies (from the first
2128
- `prepublish` in the main package
2229
- `prepare` in the main package
2330

24-
Allowed package list is configurable in `package.json` by adding an `allowScripts` property, with an object where the key is a package name and the value is one of:
25-
26-
* a string with a semver specifier for allowed versions
27-
- non-matching versions will be ignored
28-
* `true` - allow all versions (equivalent to `'*'` semver specifier)
29-
* `false` - ignore all versions
30-
31-
If a package has a lifecycle script, but is neither allowed nor ignored, `allow-scripts` will exit with an error.
31+
### Configuration
3232

33-
Example for `package.json`:
3433
```
3534
"allowScripts": {
3635
"fsevents": "*", # allow install scripts in all versions
3736
"node-sass": false, # ignore install scripts for all versions
3837
"webpack-cli": "3.x.x" # allow all minors for v3, ignore everything else
3938
}
4039
```
40+
41+
Allowed package list is configurable in `package.json` by adding an `allowScripts` property, with an object where the key is a package name and the value is one of:
42+
43+
* a string with a semver specifier for allowed versions
44+
- non-matching versions will be ignored
45+
* `true` - allow all versions (equivalent to `'*'` semver specifier)
46+
* `false` - ignore all versions
47+
48+
If a package has a lifecycle script, but is neither allowed nor ignored, `allow-scripts` will exit with an error.

lib/index.js

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,14 @@ internals.queue = (tree) => {
5151

5252
internals.runScript = (stage, { pkg, path, cwd, unsafePerm }, options) => {
5353

54+
if (!pkg.scripts || !pkg.scripts[stage]) {
55+
return;
56+
}
57+
5458
console.log();
5559

5660
if (options.dryRun) {
57-
console.log(`DRY RUN ==> ${stage} ${path || pkg.name}...`);
61+
console.log(`DRY RUN ==> ${stage} ${path || pkg.name}`);
5862
return;
5963
}
6064

@@ -84,21 +88,13 @@ internals.getLockFile = (cwd) => {
8488
return require(Path.join(cwd, 'package-lock.json'));
8589
}
8690

87-
let output;
88-
try {
89-
output = Cp.execSync('npm ls --json', { cwd });
90-
}
91-
catch (err) {
92-
output = err.output[1]; // npm will exist with an error when e.g. there's peer deps missing - attempt to ignore that
93-
}
91+
Cp.execSync('npm shrinkwrap');
9492

95-
try {
96-
return JSON.parse(output.toString());
97-
}
98-
catch (err) {
99-
console.error(err);
100-
throw new Error('Failed to read the contents of node_modules');
101-
}
93+
const lockFilePath = Path.join(cwd, 'npm-shrinkwrap.json');
94+
const lockFileContents = require(lockFilePath);
95+
96+
Fs.unlinkSync(lockFilePath);
97+
return lockFileContents;
10298
};
10399

104100
exports.run = async (options) => {
@@ -108,7 +104,13 @@ exports.run = async (options) => {
108104

109105
pkg._id = `${pkg.name}@${pkg.version}`; // @todo: find an official way to do this for top level package
110106

111-
const tree = Npm.logicalTree(pkg, internals.getLockFile(cwd));
107+
try {
108+
var tree = Npm.logicalTree(pkg, internals.getLockFile(cwd));
109+
}
110+
catch (err) {
111+
throw new Error('Failed to read the installed tree - you might want to `rm -rf node_modules && npm i --ignore-scripts`.');
112+
}
113+
112114
const queue = internals.queue(tree);
113115

114116
const allowScripts = pkg.allowScripts || {};
@@ -134,6 +136,7 @@ exports.run = async (options) => {
134136

135137
if (allowScripts[name] === false) {
136138
console.warn(`==========> skip ${path} (because it is not allowed in package.json)`);
139+
return false;
137140
}
138141

139142
if (allowScripts[name] === true) {

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"url": "https://github.com/dominykas/allow-scripts.git"
88
},
99
"scripts": {
10-
"test": "lab -L -n"
10+
"test": "lab -L -t 100 -m 5000"
1111
},
1212
"bin": {
1313
"allow-scripts": "./bin/allow-scripts.js"
@@ -20,8 +20,12 @@
2020
"topo": "3.x.x"
2121
},
2222
"devDependencies": {
23+
"code": "5.x.x",
2324
"lab": "18.x.x",
24-
"semantic-release": "15.x.x"
25+
"mkdirp": "0.5.x",
26+
"rimraf": "2.x.x",
27+
"semantic-release": "15.x.x",
28+
"sinon": "7.x.x"
2529
},
2630
"files": [
2731
"bin",

test/fixtures/allowed-false.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"private": true,
3+
"name": "@example/allowed-false",
4+
"version": "0.0.0",
5+
"dependencies": {
6+
"@example/with-install-script": "*",
7+
"@example/with-postinstall-script": "*",
8+
"@example/without-scripts": "*"
9+
},
10+
"allowScripts": {
11+
"@example/with-install-script": false,
12+
"@example/with-postinstall-script": "*"
13+
}
14+
}

test/fixtures/allowed-semver.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"private": true,
3+
"name": "@example/allowed-semver",
4+
"version": "0.0.0",
5+
"dependencies": {
6+
"@example/with-install-script": "*",
7+
"@example/with-postinstall-script": "*",
8+
"@example/without-scripts": "*"
9+
},
10+
"allowScripts": {
11+
"@example/with-install-script": "1.x.x",
12+
"@example/with-postinstall-script": "*"
13+
}
14+
}

test/fixtures/basic.dry-run.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
DRY RUN ==> preinstall @example/basic
2+
3+
DRY RUN ==> preinstall node_modules/@example/with-preinstall-script
4+
5+
DRY RUN ==> install node_modules/@example/with-install-script
6+
7+
DRY RUN ==> postinstall node_modules/@example/with-postinstall-script
8+
9+
DRY RUN ==> install @example/basic
10+
11+
DRY RUN ==> postinstall @example/basic
12+
13+
DRY RUN ==> prepublish @example/basic
14+
15+
DRY RUN ==> prepare @example/basic

test/fixtures/basic.full.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
preinstall from basic
2+
preinstall from with-preinstall-script
3+
install from with-install-script
4+
postinstall from with-postinstall-script
5+
install from basic
6+
postinstall from basic
7+
prepublish from basic
8+
prepare from basic

test/fixtures/basic.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"private": true,
3+
"name": "@example/basic",
4+
"version": "0.0.0",
5+
"dependencies": {
6+
"@example/with-preinstall-script": "*",
7+
"@example/with-install-script": "*",
8+
"@example/with-postinstall-script": "*",
9+
"@example/without-scripts": "*",
10+
"@example/without-install-scripts": "*"
11+
},
12+
"allowScripts": {
13+
"@example/with-preinstall-script": "*",
14+
"@example/with-install-script": "*",
15+
"@example/with-postinstall-script": true
16+
},
17+
"scripts": {
18+
"preinstall": "echo preinstall from basic >> ${OUTPUT}",
19+
"install": "echo install from basic >> ${OUTPUT}",
20+
"postinstall": "echo postinstall from basic >> ${OUTPUT}",
21+
"prepublish": "echo prepublish from basic >> ${OUTPUT}",
22+
"prepare": "echo prepare from basic >> ${OUTPUT}"
23+
}
24+
}

test/fixtures/cycle-a.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"private": true,
3+
"name": "@example/cycle-a",
4+
"version": "0.0.0",
5+
"dependencies": {
6+
"@example/cycle-b": "*"
7+
}
8+
}

test/fixtures/cycle-b.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"private": true,
3+
"name": "@example/cycle-b",
4+
"version": "0.0.0",
5+
"dependencies": {
6+
"@example/cycle-a": "*"
7+
}
8+
}

test/fixtures/deep.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"private": true,
3+
"name": "@example/deep",
4+
"version": "0.0.0",
5+
"dependencies": {
6+
"@example/basic": "*",
7+
"@example/with-preinstall-script": "*"
8+
},
9+
"scripts": {},
10+
"allowScripts": {
11+
"@example/basic": "*",
12+
"@example/with-preinstall-script": "*",
13+
"@example/with-install-script": "*",
14+
"@example/with-postinstall-script": true
15+
}
16+
}

test/fixtures/deep.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
preinstall from with-preinstall-script
2+
preinstall from basic
3+
install from with-install-script
4+
install from basic
5+
postinstall from with-postinstall-script
6+
postinstall from basic

test/fixtures/index.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
'use strict';
2+
3+
4+
const Fs = require('fs');
5+
const Mkdirp = require('mkdirp');
6+
const Path = require('path');
7+
const Rimraf = require('rimraf');
8+
const Sinon = require('sinon');
9+
10+
11+
const internals = {
12+
restore: []
13+
};
14+
15+
16+
internals.readFile = (path) => Fs.readFileSync(path).toString().trim();
17+
18+
19+
exports.expectedResults = {
20+
basicFull: internals.readFile(Path.join(__dirname, 'basic.full.txt')),
21+
basicDryRun: internals.readFile(Path.join(__dirname, 'basic.dry-run.txt')),
22+
deep: internals.readFile(Path.join(__dirname, 'deep.txt'))
23+
};
24+
25+
26+
exports.setup = (main, deps) => {
27+
28+
const cwd = Path.join(__dirname, '..', 'tmp');
29+
const output = Path.join(cwd, 'res.txt');
30+
31+
Rimraf.sync(cwd);
32+
Mkdirp.sync(cwd);
33+
Fs.copyFileSync(Path.join(__dirname, `${main}.json`), Path.join(cwd, 'package.json'));
34+
Fs.writeFileSync(Path.join(cwd, 'res.txt'), '');
35+
36+
deps.forEach((dep) => {
37+
38+
const pkg = require(`./${dep}.json`);
39+
40+
Mkdirp.sync(Path.join(cwd, 'node_modules', pkg.name));
41+
const pkgJsonPath = Path.join(cwd, 'node_modules', pkg.name, 'package.json');
42+
Fs.writeFileSync(pkgJsonPath, JSON.stringify(Object.assign({}, pkg, {
43+
_id: `${pkg.name}@${pkg.version}`
44+
})));
45+
});
46+
47+
process.chdir(cwd);
48+
49+
const originalOutput = process.env.output;
50+
process.env.OUTPUT = output;
51+
52+
internals.restore.push(() => {
53+
54+
process.env.OUTPUT = originalOutput;
55+
Object.keys(require.cache).forEach((k) => {
56+
57+
if (k.startsWith(cwd)) {
58+
delete require.cache[k];
59+
}
60+
});
61+
});
62+
63+
const log = [];
64+
const appendLog = (...items) => {
65+
66+
// @todo: should suppress this in production code
67+
if (items[0] === 'npm notice created a lockfile as npm-shrinkwrap.json. You should commit this file.\n') {
68+
return;
69+
}
70+
71+
log.push(items.map((i) => i || '').join(' ').replace(new RegExp(cwd, 'g'), '.'));
72+
};
73+
74+
Sinon.stub(console, 'info').callsFake(appendLog);
75+
Sinon.stub(console, 'log').callsFake(appendLog);
76+
Sinon.stub(console, 'warn').callsFake(appendLog);
77+
Sinon.stub(process.stderr, 'write').callsFake((data) => appendLog(data.toString()));
78+
79+
return {
80+
cwd,
81+
getActualResult: () => internals.readFile(output),
82+
getLog: () => log.join('\n').trim()
83+
};
84+
};
85+
86+
exports.restore = () => {
87+
88+
internals.restore.forEach((restore) => restore());
89+
internals.restore = [];
90+
Sinon.restore();
91+
};

test/fixtures/invalid-semver.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"private": true,
3+
"name": "@example/invalid-semver",
4+
"version": "0.0.0",
5+
"dependencies": {
6+
"@example/with-install-script": "*",
7+
"@example/with-postinstall-script": "*",
8+
"@example/without-scripts": "*"
9+
},
10+
"allowScripts": {
11+
"@example/with-install-script": "not-a-semver-range",
12+
"@example/with-postinstall-script": "*"
13+
}
14+
}

test/fixtures/not-in-allowed.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"private": true,
3+
"name": "@example/not-in-allowed",
4+
"version": "0.0.0",
5+
"dependencies": {
6+
"@example/with-install-script": "*",
7+
"@example/with-postinstall-script": "*",
8+
"@example/without-scripts": "*"
9+
},
10+
"allowScripts": {
11+
"@example/with-postinstall-script": "*"
12+
}
13+
}

0 commit comments

Comments
 (0)