-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathindex.js
182 lines (148 loc) · 6.35 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
'use strict';
var fs = require('fs'),
npa = require('npm-package-arg'),
satisfies = require('spdx-satisfies'),
validate = require('spdx-expression-validate'),
semver = require('semver'),
format = require('util').format;
/* istanbul ignore next */
function stringsort(a, b) {
if (a < b) { return -1; }
if (a > b) { return 1; }
return 0;
}
function validateArgs(rootDir, opts, cb) { // jshint maxcomplexity: 12
function fail(err) {
if (typeof cb === 'function') { cb(err); }
else { throw err; }
return true;
}
if (typeof rootDir !== 'string') {
return fail(Error('nlf-validator: invalid rootDir: ' + rootDir));
}
try {
var stat = fs.statSync(rootDir);
if (!stat.isDirectory()) {
return fail(new Error('nlf-validator: not a directory: ' + rootDir));
}
} catch (e) {
return fail(new Error('nlf-validator: invalid rootDir: ' + rootDir + ': ' + e.message));
}
if (!opts || typeof opts !== 'object') {
return fail(new Error('nlf-validator: invalid options: ' + opts));
}
if (!cb || typeof cb !== 'function') {
return fail(new Error('nlf-validator: no callback specified'));
}
if (opts.listOnly) { return; }
if ((!Array.isArray(opts.licenses) ? 0 : opts.licenses.length) +
(!Array.isArray(opts.packages) ? 0 : opts.packages.length) === 0) {
return fail(new Error('nlf-validator: no licenses or packages specified'));
}
}
module.exports = function (rootDir, opts, cb) {
if (validateArgs(rootDir, opts, cb)) { return; }
var warn = opts.warn || function () { };
var nlf = opts.__nlf || require('nlf');
opts.licenses = opts.licenses || [];
opts.packages = opts.packages || [];
var whitelistLicenses = opts.licenses.reduce(function (acc, cur) {
acc[cur.toLowerCase()] = cur;
return acc;
}, {});
// not all of the licenses we specify might be valid spdx identifiers
// but when we are comparing licenses, package.json may specify spdx expressions
// so we need to construct something to compare against spdx expressions
// our comparison must contain only valid identifiers
// we build a string like (MIT OR ISC OR JSON OR BSD-3-Clause) with all the licenses
// we have deemed acceptable; if a package has an 'AND' specification, it will validate
// correctly as long as we specify both parts
var whitelistSPDX = '(' + opts.licenses.filter(validate).join(' OR ') + ')';
var whitelistPackages = opts.packages.reduce(function (acc, cur) {
var parsed = npa(cur);
var pkg = parsed.name;
if (acc.hasOwnProperty(pkg)) {
warn(pkg + ' is specified more than once in allowed packages. Use the SPDX `||` operator to specify multiple versions of a single package.');
}
if (parsed.rawSpec === '') {
parsed.rawSpec = '*';
}
acc[pkg] = parsed;
return acc;
}, {});
nlf.find({
directory: rootDir,
depth: opts.deep ? Infinity : 0,
production: !!opts.production
}, function (err, data) {
if (err) { cb(err); return; }
var PACKAGES = {},
LICENSES = {},
INVALIDS = [];
if (!data || !Array.isArray(data)) {
cb(new Error('NLF returned invalid data'));
return;
}
if (data.length === 0) {
cb(new Error('NLF found no licenses'));
return;
}
// nlf's return data sucks, and the standard formatter does a lot more than just making it pretty
// so instead we just call the formatter and parse the return data
nlf.standardFormatter.render(data, {}, function (err, output) {
if (err) { cb(err); return; }
var re = new RegExp(/(.*?@\d+\.[^ ]+) \[license\(s\): (.*)\]$/mg),
match, packages = [], seenPackages = {};
while ((match = re.exec(output))) {
packages.push(match);
}
packages.forEach(function (match) {
// get the first license in the whitelist that applies to this package
var pkgName = npa(match[1]).name.toLowerCase();
seenPackages[pkgName] = 1;
var license = match[2].split(', ').reduce(function (acc, cur) {
if (acc) { return acc; }
// if the license from the file is a valid spdx expression,
// compare it against our spdx whitelist expression
if (validate(cur) && validate(whitelistSPDX) && satisfies(cur, whitelistSPDX)) {
return cur;
}
// otherwise do a simple string comparison
if (cur.toLowerCase() in whitelistLicenses) {
return cur;
}
return null;
}, null);
if (license !== null) {
PACKAGES[match[1]] = license;
LICENSES[license] = (LICENSES[license] || 0) + 1;
return;
}
var parsed = npa(match[1]);
if (parsed.rawSpec && parsed.name in whitelistPackages) {
var target = whitelistPackages[parsed.name],
spec = parsed.rawSpec.replace(/[-+].*/, ''); // semver won't match '*' against prerelease versions
if (semver.satisfies(spec, target.rawSpec)) {
PACKAGES[match[1]] = format('%s (exception: %s)', match[2], target.raw);
return;
}
}
LICENSES[match[2]] = (LICENSES[match[2]] || 0) + 1;
PACKAGES[match[1]] = match[2];
INVALIDS.push(match[1]);
});
var unseenPackages = Object.keys(whitelistPackages).reduce(function (acc, cur) {
if (!seenPackages[cur]) { acc.push(cur); }
return acc;
}, []);
if (unseenPackages.length) {
warn('Packages were listed as exceptions but not found in the project: ' + unseenPackages.join(', '));
}
cb(null, {
packages: PACKAGES,
licenses: Object.keys(LICENSES).sort(stringsort),
invalids: INVALIDS
});
});
});
};