Skip to content

Commit 07a350e

Browse files
committed
Make big improvements and add tests.
Fixes #3 . This package now supports npm v7+. While Node.js v12 and v14 should be supported (with npm updated to v7+), this isn’t easy to test in GitHub Actions CI as actions/setup-node@v2 doesn’t allow the npm version to be configured, see: actions/setup-node#213 .
1 parent 1635252 commit 07a350e

File tree

74 files changed

+2209
-166
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+2209
-166
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/test/fixtures/package-json-broken

.github/workflows/ci.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,15 @@ jobs:
77
strategy:
88
matrix:
99
os: [ubuntu-latest, macos-latest]
10-
node: ['12', '14', '16']
10+
node:
11+
# This package supports npm v7+. While Node.js v12 and v14 should be
12+
# supported (with npm updated to v7+), this isn’t easy to test in
13+
# GitHub Actions CI as actions/setup-node@v2 doesn’t allow the npm
14+
# version to be configured, see:
15+
# https://github.com/actions/setup-node/issues/213
16+
# - '12'
17+
# - '14'
18+
- '16'
1119
steps:
1220
- uses: actions/checkout@v2
1321
- name: Setup Node.js v${{ matrix.node }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
node_modules
22
.DS_Store
3+
!/test/fixtures/**/node_modules

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
package.json
2+
/test/snapshots

changelog.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,51 @@
55
### Major
66

77
- Updated Node.js support to `^12.20 || >= 14.13`.
8+
- Updated npm support to `>= 7`.
89
- Updated dependencies, some of which require newer Node.js versions than were previously supported.
910
- Published modules are now ESM in `.mjs` files instead of CJS in `.js` files.
11+
- Added a package `exports` field.
12+
- Stop displaying the execution time in CLI output.
13+
- Added tests with 100% code coverage, using ESM in `.mjs` files, a new package `test:api` script, and new dev dependencies:
14+
- [`coverage-node`](https://npm.im/coverage-node)
15+
- [`snapshot-assertion`](https://npm.im/snapshot-assertion)
16+
- [`test-director`](https://npm.im/test-director)
1017

1118
### Minor
1219

20+
- Added a package `sideEffects` field.
21+
- Added a package `main` field and a public JS API.
22+
- Added a new “unknown” age category to `audit-age` command output, and correctly handle packages without a published date (e.g. it’s a local file or Git dependency).
23+
- Handle errors and display them with nice formatting in CLI output.
1324
- Setup [GitHub Sponsors funding](https://github.com/sponsors/jaydenseric):
1425
- Added `.github/funding.yml` to display a sponsor button in GitHub.
1526
- Added a package `funding` field to enable npm CLI funding features.
1627

1728
### Patch
1829

30+
- Use [`duration-relativetimeformat`](https://npm.im/duration-relativetimeformat) instead of [`moment`](https://npm.im/moment) to format durations.
31+
- Use [`kleur`](https://npm.im/kleur) instead of [`chalk`](https://npm.im/chalk) to format CLI output with ANSI colors.
1932
- Stop using [`husky`](https://npm.im/husky) and [`lint-staged`](https://npm.im/lint-staged).
33+
- Removed the [`cli-table3`](https://npm.im/cli-table3) dependency and manually align CLI output instead.
34+
- Use [`jsdoc-md`](https://npm.im/jsdoc-md) to generate and check the new readme section “API” via new package `jsdoc` and `test:jsdoc` scripts.
35+
- Updated the package description.
2036
- Updated the package `keywords` field.
2137
- Removed the package `engines.npm` field.
2238
- More specific package `bin` field.
2339
- Improved the package scripts.
2440
- Moved dev config from `package.json` to separate files, for a leaner install size.
41+
- Only audit deduped packages once to massively speedup audits and simplify output.
42+
- Made the `audit-age` CLI output the audit in smaller `stdout` chunks instead of all at once, fixing [#3](https://github.com/jaydenseric/audit-age/issues/3).
43+
- Use the more efficient Node.js `child_process` function `execFile` instead of `exec` when running the npm CLI.
44+
- Removed redundant `--only production` args from the `npm ls` command used to get the dependency tree for the package being audited.
2545
- Configured Prettier option `semi` to the default, `true`.
2646
- Use GitHub Actions instead of Travis for CI.
2747
- Updated the EditorConfig.
2848
- Removed `package-lock.json` from the `.gitignore` and `.prettierignore` files as it’s disabled in `.npmrc` anyway.
2949
- Removed `npm-debug.log` from the `.gitignore` file as npm [v4.2.0](https://github.com/npm/npm/releases/tag/v4.2.0)+ doesn’t create it in the current working directory.
3050
- Use [Badgen](https://badgen.net) instead of [Shields](https://shields.io) for the readme npm version badge.
3151
- Removed the readme section “Demo”.
52+
- Improved documentation.
3253
- Amended the changelog entry for v0.1.1.
3354

3455
## 0.1.1

cli/audit-age.mjs

Lines changed: 112 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -1,169 +1,128 @@
11
#!/usr/bin/env node
22

3-
import { exec } from 'child_process';
4-
import { promisify } from 'util';
5-
import chalk from 'chalk';
6-
import Table from 'cli-table3';
7-
import moment from 'moment';
8-
9-
const asyncExec = promisify(exec);
10-
const startTime = new Date();
11-
const thresholds = [
12-
{
13-
label: 'Day',
14-
count: 0,
15-
ms: 8.64e7,
16-
color: 'green',
17-
},
18-
{
19-
label: 'Week',
20-
count: 0,
21-
ms: 6.048e8,
22-
color: 'cyan',
23-
},
24-
{
25-
label: 'Month',
26-
count: 0,
27-
ms: 2.628e9,
28-
color: 'magenta',
29-
},
30-
{
31-
label: 'Year',
32-
count: 0,
33-
ms: 3.154e10,
34-
color: 'yellow',
35-
},
36-
{
37-
label: 'Year+',
38-
count: 0,
39-
ms: Infinity,
40-
color: 'red',
41-
},
42-
];
43-
const clearTableChars = {
44-
top: '',
45-
'top-mid': '',
46-
'top-left': '',
47-
'top-right': '',
48-
bottom: '',
49-
'bottom-mid': '',
50-
'bottom-left': '',
51-
'bottom-right': '',
52-
left: '',
53-
'left-mid': '',
54-
mid: '',
55-
'mid-mid': '',
56-
right: '',
57-
'right-mid': '',
58-
middle: '',
59-
};
3+
import createFormatDuration from 'duration-relativetimeformat';
4+
import kleur from 'kleur';
5+
import reportCliError from '../private/reportCliError.mjs';
6+
import auditAge from '../public/auditAge.mjs';
7+
8+
const formatDuration = createFormatDuration('en-US');
609

6110
/**
62-
* Audits the age of installed npm packages.
63-
* @private
11+
* Runs the `audit-age` CLI.
12+
* @kind function
13+
* @name auditAgeCli
14+
* @returns {Promise<void>} Resolves once the operation is done.
15+
* @ignore
6416
*/
65-
async function auditAge() {
66-
const { stdout: rawTree } = await asyncExec(
67-
'npm ls --prod --only production --json'
68-
);
69-
const tree = JSON.parse(rawTree);
70-
const lookups = [];
71-
72-
/**
73-
* Recurses dependencies to prepare the report.
74-
* @param {Array<object>} dependencies Dependencies nested at the current level.
75-
* @param {Array<string>} ancestorPath How the dependency is nested.
76-
*/
77-
const recurse = (dependencies, ancestorPath = []) => {
78-
Object.entries(dependencies).forEach(
79-
([name, { version, dependencies }]) => {
80-
const path = [...ancestorPath, `${name}@${version}`];
81-
lookups.push(
82-
asyncExec(`npm view ${name} time --json`).then(
83-
({ stdout: rawTimes }) => {
84-
const times = JSON.parse(rawTimes);
85-
const published = moment(times[version]);
86-
const msDiff = moment(startTime).diff(published);
87-
const threshold = thresholds.find(({ ms }) => msDiff < ms);
88-
threshold.count++;
89-
return {
90-
path,
91-
name,
92-
version,
93-
published,
94-
threshold,
95-
};
96-
}
97-
)
98-
);
99-
if (dependencies) recurse(dependencies, path);
100-
}
101-
);
102-
};
103-
104-
recurse(tree.dependencies);
105-
106-
// eslint-disable-next-line no-console
107-
console.log(`\nFetching ${lookups.length} package ages...\n`);
17+
async function auditAgeCli() {
18+
try {
19+
console.info('Auditing the age of installed production npm packages…');
10820

109-
const list = await Promise.all(lookups);
110-
const sorted = list.sort((a, b) => a.published - b.published);
111-
const packagesTable = new Table({
112-
chars: {
113-
...clearTableChars,
114-
mid: '─',
115-
'mid-mid': '─',
116-
},
117-
});
118-
119-
sorted.forEach(({ path, published, threshold }) =>
120-
packagesTable.push([
21+
const dateAudit = new Date();
22+
const audit = await auditAge();
23+
const unknownCategory = {
24+
label: 'Unknown',
25+
color: 'grey',
26+
count: 0,
27+
};
28+
const thresholdCategories = [
12129
{
122-
vAlign: 'bottom',
123-
content: path.reduce((tree, item, index) => {
124-
if (index > 0) tree += `\n${' '.repeat(index - 1)}└─ `;
125-
return (index === path.length - 1 ? chalk.dim(tree) : tree) + item;
126-
}, ''),
30+
label: 'Day',
31+
ms: 8.64e7,
32+
color: 'green',
33+
count: 0,
12734
},
12835
{
129-
hAlign: 'right',
130-
vAlign: 'bottom',
131-
content: `${chalk[threshold.color](
132-
published.fromNow()
133-
)}\n${published.format('lll')}`,
36+
label: 'Week',
37+
ms: 6.048e8,
38+
color: 'cyan',
39+
count: 0,
13440
},
135-
])
136-
);
137-
138-
// eslint-disable-next-line no-console
139-
console.log(`${packagesTable.toString()}\n\n`);
140-
141-
const summaryTable = new Table({
142-
chars: clearTableChars,
143-
});
144-
145-
thresholds.reverse().forEach(({ color, label, count }) =>
146-
summaryTable.push([
14741
{
148-
hAlign: 'right',
149-
content: chalk[color](label),
42+
label: 'Month',
43+
ms: 2.628e9,
44+
color: 'magenta',
45+
count: 0,
15046
},
15147
{
152-
hAlign: 'right',
153-
content: count,
48+
label: 'Year',
49+
ms: 3.154e10,
50+
color: 'yellow',
51+
count: 0,
15452
},
155-
])
156-
);
157-
158-
// eslint-disable-next-line no-console
159-
console.log(`${summaryTable.toString()}\n`);
160-
161-
// eslint-disable-next-line no-console
162-
console.log(
163-
`Audited ${lookups.length} package ages in ${
164-
(new Date() - startTime) / 1000
165-
}s.\n`
166-
);
53+
{
54+
label: 'Year+',
55+
ms: Infinity,
56+
color: 'red',
57+
count: 0,
58+
},
59+
];
60+
61+
for (const { path, datePublished } of audit) {
62+
let category;
63+
64+
if (datePublished) {
65+
const msDiff = dateAudit - datePublished;
66+
67+
category = thresholdCategories.find(({ ms }) => msDiff < ms);
68+
} else category = unknownCategory;
69+
70+
category.count++;
71+
72+
let dependencyTree = '';
73+
74+
path.forEach(({ name, version }, index) => {
75+
if (index)
76+
dependencyTree += `
77+
${' '.repeat(index - 1)}└─ `;
78+
79+
if (index === path.length - 1)
80+
dependencyTree = kleur.dim(dependencyTree);
81+
82+
dependencyTree += name;
83+
84+
if (version) dependencyTree += `@${version}`;
85+
});
86+
87+
console.info(`
88+
${dependencyTree}
89+
${kleur[category.color](
90+
`${kleur.dim(datePublished ? datePublished.toISOString() : 'Unavailable')} (${
91+
datePublished ? formatDuration(datePublished, dateAudit) : 'unknown age'
92+
})`
93+
)}`);
94+
}
95+
96+
const allCategories = [...thresholdCategories.reverse(), unknownCategory];
97+
98+
// This is needed to align the category count column.
99+
const longestCategoryLabelLength = Math.max(
100+
...allCategories.map(({ label }) => label.length)
101+
);
102+
103+
let outputSummary = '';
104+
105+
for (const { label, color, count } of allCategories)
106+
outputSummary += `
107+
${' '.repeat(longestCategoryLabelLength - label.length)}${kleur[color](
108+
label
109+
)} ${count}`;
110+
111+
outputSummary += `
112+
113+
${kleur.bold(
114+
`Audited the age of ${audit.length} installed production npm package${
115+
audit.length === 1 ? '' : 's'
116+
}.`
117+
)}
118+
`;
119+
120+
console.info(outputSummary);
121+
} catch (error) {
122+
reportCliError('audit-age', error);
123+
124+
process.exitCode = 1;
125+
}
167126
}
168127

169-
auditAge();
128+
auditAgeCli();

0 commit comments

Comments
 (0)