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

Commit

Permalink
Merge pull request #16 from dominykas/tests
Browse files Browse the repository at this point in the history
Add tests
  • Loading branch information
dominykas committed Feb 11, 2019
2 parents 6f847d7 + 964ff7e commit 232555c
Show file tree
Hide file tree
Showing 24 changed files with 563 additions and 31 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
.idea
node_modules

test/tmp/
32 changes: 20 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@

Execute allowed `npm install` lifecycle scripts.

## Usage
## tl;dr

- Whitelist packages that you trust in your `package.json`: `"allowScripts": { "packageName": "1.x.x - 2.x.x" }`
- Run `npm install --ignore-scripts` or `yarn install --ignore-scripts`
- Run `npx allow-scripts`

Only the explicitly allowed `[pre|post]install` scripts will be executed.

Run your `npm install` with `--ignore-scripts` (or add `ignore-scripts=true` in your `.npmrc`), then:

## Usage

```
$ npx allow-scripts [--dry-run]
```

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:
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:

- `preinstall` in the main package
- `preinstall` in dependencies
Expand All @@ -21,20 +28,21 @@ Running the command will scan the list of installed dependencies (from the first
- `prepublish` in the main package
- `prepare` in the main package

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:

* a string with a semver specifier for allowed versions
- non-matching versions will be ignored
* `true` - allow all versions (equivalent to `'*'` semver specifier)
* `false` - ignore all versions

If a package has a lifecycle script, but is neither allowed nor ignored, `allow-scripts` will exit with an error.
### Configuration

Example for `package.json`:
```
"allowScripts": {
"fsevents": "*", # allow install scripts in all versions
"node-sass": false, # ignore install scripts for all versions
"webpack-cli": "3.x.x" # allow all minors for v3, ignore everything else
}
```

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:

* a string with a semver specifier for allowed versions
- non-matching versions will be ignored
* `true` - allow all versions (equivalent to `'*'` semver specifier)
* `false` - ignore all versions

If a package has a lifecycle script, but is neither allowed nor ignored, `allow-scripts` will exit with an error.
35 changes: 19 additions & 16 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,14 @@ internals.queue = (tree) => {

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

if (!pkg.scripts || !pkg.scripts[stage]) {
return;
}

console.log();

if (options.dryRun) {
console.log(`DRY RUN ==> ${stage} ${path || pkg.name}...`);
console.log(`DRY RUN ==> ${stage} ${path || pkg.name}`);
return;
}

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

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

try {
return JSON.parse(output.toString());
}
catch (err) {
console.error(err);
throw new Error('Failed to read the contents of node_modules');
}
const lockFilePath = Path.join(cwd, 'npm-shrinkwrap.json');
const lockFileContents = require(lockFilePath);

Fs.unlinkSync(lockFilePath);
return lockFileContents;
};

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

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

const tree = Npm.logicalTree(pkg, internals.getLockFile(cwd));
try {
var tree = Npm.logicalTree(pkg, internals.getLockFile(cwd));
}
catch (err) {
throw new Error('Failed to read the installed tree - you might want to `rm -rf node_modules && npm i --ignore-scripts`.');
}

const queue = internals.queue(tree);

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

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

if (allowScripts[name] === true) {
Expand Down
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/dominykas/allow-scripts.git"
},
"scripts": {
"test": "lab -L -n"
"test": "lab -L -t 100 -m 5000"
},
"bin": {
"allow-scripts": "./bin/allow-scripts.js"
Expand All @@ -20,8 +20,12 @@
"topo": "3.x.x"
},
"devDependencies": {
"code": "5.x.x",
"lab": "18.x.x",
"semantic-release": "15.x.x"
"mkdirp": "0.5.x",
"rimraf": "2.x.x",
"semantic-release": "15.x.x",
"sinon": "7.x.x"
},
"files": [
"bin",
Expand Down
14 changes: 14 additions & 0 deletions test/fixtures/allowed-false.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"private": true,
"name": "@example/allowed-false",
"version": "0.0.0",
"dependencies": {
"@example/with-install-script": "*",
"@example/with-postinstall-script": "*",
"@example/without-scripts": "*"
},
"allowScripts": {
"@example/with-install-script": false,
"@example/with-postinstall-script": "*"
}
}
14 changes: 14 additions & 0 deletions test/fixtures/allowed-semver.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"private": true,
"name": "@example/allowed-semver",
"version": "0.0.0",
"dependencies": {
"@example/with-install-script": "*",
"@example/with-postinstall-script": "*",
"@example/without-scripts": "*"
},
"allowScripts": {
"@example/with-install-script": "1.x.x",
"@example/with-postinstall-script": "*"
}
}
15 changes: 15 additions & 0 deletions test/fixtures/basic.dry-run.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
DRY RUN ==> preinstall @example/basic

DRY RUN ==> preinstall node_modules/@example/with-preinstall-script

DRY RUN ==> install node_modules/@example/with-install-script

DRY RUN ==> postinstall node_modules/@example/with-postinstall-script

DRY RUN ==> install @example/basic

DRY RUN ==> postinstall @example/basic

DRY RUN ==> prepublish @example/basic

DRY RUN ==> prepare @example/basic
8 changes: 8 additions & 0 deletions test/fixtures/basic.full.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
preinstall from basic
preinstall from with-preinstall-script
install from with-install-script
postinstall from with-postinstall-script
install from basic
postinstall from basic
prepublish from basic
prepare from basic
24 changes: 24 additions & 0 deletions test/fixtures/basic.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"private": true,
"name": "@example/basic",
"version": "0.0.0",
"dependencies": {
"@example/with-preinstall-script": "*",
"@example/with-install-script": "*",
"@example/with-postinstall-script": "*",
"@example/without-scripts": "*",
"@example/without-install-scripts": "*"
},
"allowScripts": {
"@example/with-preinstall-script": "*",
"@example/with-install-script": "*",
"@example/with-postinstall-script": true
},
"scripts": {
"preinstall": "echo preinstall from basic >> ${OUTPUT}",
"install": "echo install from basic >> ${OUTPUT}",
"postinstall": "echo postinstall from basic >> ${OUTPUT}",
"prepublish": "echo prepublish from basic >> ${OUTPUT}",
"prepare": "echo prepare from basic >> ${OUTPUT}"
}
}
8 changes: 8 additions & 0 deletions test/fixtures/cycle-a.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"private": true,
"name": "@example/cycle-a",
"version": "0.0.0",
"dependencies": {
"@example/cycle-b": "*"
}
}
8 changes: 8 additions & 0 deletions test/fixtures/cycle-b.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"private": true,
"name": "@example/cycle-b",
"version": "0.0.0",
"dependencies": {
"@example/cycle-a": "*"
}
}
16 changes: 16 additions & 0 deletions test/fixtures/deep.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"private": true,
"name": "@example/deep",
"version": "0.0.0",
"dependencies": {
"@example/basic": "*",
"@example/with-preinstall-script": "*"
},
"scripts": {},
"allowScripts": {
"@example/basic": "*",
"@example/with-preinstall-script": "*",
"@example/with-install-script": "*",
"@example/with-postinstall-script": true
}
}
6 changes: 6 additions & 0 deletions test/fixtures/deep.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
preinstall from with-preinstall-script
preinstall from basic
install from with-install-script
install from basic
postinstall from with-postinstall-script
postinstall from basic
91 changes: 91 additions & 0 deletions test/fixtures/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
'use strict';


const Fs = require('fs');
const Mkdirp = require('mkdirp');
const Path = require('path');
const Rimraf = require('rimraf');
const Sinon = require('sinon');


const internals = {
restore: []
};


internals.readFile = (path) => Fs.readFileSync(path).toString().trim();


exports.expectedResults = {
basicFull: internals.readFile(Path.join(__dirname, 'basic.full.txt')),
basicDryRun: internals.readFile(Path.join(__dirname, 'basic.dry-run.txt')),
deep: internals.readFile(Path.join(__dirname, 'deep.txt'))
};


exports.setup = (main, deps) => {

const cwd = Path.join(__dirname, '..', 'tmp');
const output = Path.join(cwd, 'res.txt');

Rimraf.sync(cwd);
Mkdirp.sync(cwd);
Fs.copyFileSync(Path.join(__dirname, `${main}.json`), Path.join(cwd, 'package.json'));
Fs.writeFileSync(Path.join(cwd, 'res.txt'), '');

deps.forEach((dep) => {

const pkg = require(`./${dep}.json`);

Mkdirp.sync(Path.join(cwd, 'node_modules', pkg.name));
const pkgJsonPath = Path.join(cwd, 'node_modules', pkg.name, 'package.json');
Fs.writeFileSync(pkgJsonPath, JSON.stringify(Object.assign({}, pkg, {
_id: `${pkg.name}@${pkg.version}`
})));
});

process.chdir(cwd);

const originalOutput = process.env.output;
process.env.OUTPUT = output;

internals.restore.push(() => {

process.env.OUTPUT = originalOutput;
Object.keys(require.cache).forEach((k) => {

if (k.startsWith(cwd)) {
delete require.cache[k];
}
});
});

const log = [];
const appendLog = (...items) => {

// @todo: should suppress this in production code
if (items[0] === 'npm notice created a lockfile as npm-shrinkwrap.json. You should commit this file.\n') {
return;
}

log.push(items.map((i) => i || '').join(' ').replace(new RegExp(cwd, 'g'), '.'));
};

Sinon.stub(console, 'info').callsFake(appendLog);
Sinon.stub(console, 'log').callsFake(appendLog);
Sinon.stub(console, 'warn').callsFake(appendLog);
Sinon.stub(process.stderr, 'write').callsFake((data) => appendLog(data.toString()));

return {
cwd,
getActualResult: () => internals.readFile(output),
getLog: () => log.join('\n').trim()
};
};

exports.restore = () => {

internals.restore.forEach((restore) => restore());
internals.restore = [];
Sinon.restore();
};
14 changes: 14 additions & 0 deletions test/fixtures/invalid-semver.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"private": true,
"name": "@example/invalid-semver",
"version": "0.0.0",
"dependencies": {
"@example/with-install-script": "*",
"@example/with-postinstall-script": "*",
"@example/without-scripts": "*"
},
"allowScripts": {
"@example/with-install-script": "not-a-semver-range",
"@example/with-postinstall-script": "*"
}
}
13 changes: 13 additions & 0 deletions test/fixtures/not-in-allowed.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"private": true,
"name": "@example/not-in-allowed",
"version": "0.0.0",
"dependencies": {
"@example/with-install-script": "*",
"@example/with-postinstall-script": "*",
"@example/without-scripts": "*"
},
"allowScripts": {
"@example/with-postinstall-script": "*"
}
}
Loading

0 comments on commit 232555c

Please sign in to comment.